mirror of
https://github.com/go-acme/lego
synced 2026-03-14 14:35:48 +01:00
Merge branch 'master'
This commit is contained in:
commit
f9455e84fc
122 changed files with 6218 additions and 376 deletions
2
.github/workflows/pr.yml
vendored
2
.github/workflows/pr.yml
vendored
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO_VERSION: stable
|
||||
GOLANGCI_LINT_VERSION: v2.9.0
|
||||
GOLANGCI_LINT_VERSION: v2.10
|
||||
HUGO_VERSION: 0.148.2
|
||||
CGO_ENABLED: 0
|
||||
LEGO_E2E_TESTS: CI
|
||||
|
|
|
|||
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -6,6 +6,36 @@ Everybody thinks that the others will donate, but in the end, nobody does.
|
|||
|
||||
So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev).
|
||||
|
||||
## v4.32.0
|
||||
|
||||
- Release date: 2026-02-19
|
||||
- Tag: [v4.32.0](https://github.com/go-acme/lego/releases/tag/v4.32.0)
|
||||
|
||||
### Added
|
||||
|
||||
- **[dnsprovider]** Add DNS provider for ArtFiles
|
||||
- **[dnsprovider]** Add DNS provider for Leaseweb
|
||||
- **[dnsprovider]** Add DNS provider for FusionLayer NameSurfer
|
||||
- **[dnsprovider]** Add DNS provider for DDNSS
|
||||
- **[dnsprovider]** Add DNS provider for Bluecat v2
|
||||
- **[dnsprovider]** Add DNS provider for TodayNIC/时代互联
|
||||
- **[dnsprovider]** Add DNS provider for DNSExit
|
||||
- **[dnsprovider]** alidns: add line record option
|
||||
|
||||
### Changed
|
||||
|
||||
- **[dnsprovider]** azure: reinforces deprecation
|
||||
- **[dnsprovider]** allinkl: detect zone through API
|
||||
|
||||
### Fixed
|
||||
|
||||
- **[ari]** fix: implement parsing for Retry-After header according to RFC 7231
|
||||
- **[dnsprovider]** namesurfer: fix updateDNSHost
|
||||
- **[dnsprovider]** timewebcloud: fix subdomain support
|
||||
- **[dnsprovider]** fix: deduplicate authz for DNS01 challenge
|
||||
- **[lib,cli]** fix: use IPs to define the main domain
|
||||
- **[lib]** fix: preserve domain order
|
||||
|
||||
## v4.31.0
|
||||
|
||||
- Release date: 2026-01-08
|
||||
|
|
|
|||
53
README.md
53
README.md
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
# Lego
|
||||
|
||||
Let's Encrypt client and ACME library written in Go.
|
||||
[ACME](https://www.rfc-editor.org/rfc/rfc8555.html) client and library for Let's Encrypt and other ACME CAs written in Go.
|
||||
|
||||
[](https://pkg.go.dev/github.com/go-acme/lego/v5)
|
||||
[](https://github.com//go-acme/lego/actions)
|
||||
|
|
@ -73,117 +73,122 @@ 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/artfiles/">ArtFiles</a></td>
|
||||
<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>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/auroradns/">Aurora DNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/autodns/">Autodns</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/axelname/">Axelname</a></td>
|
||||
<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>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/azure/">Azure (deprecated)</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/azuredns/">Azure DNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/baiducloud/">Baidu Cloud</a></td>
|
||||
<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>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/binarylane/">Binary Lane</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/bindman/">Bindman</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/bluecat/">Bluecat</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/bluecatv2/">Bluecat v2</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/bookmyname/">BookMyName</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/brandit/">Brandit (deprecated)</a></td>
|
||||
</tr><tr>
|
||||
<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/civo/">Civo</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/cloudru/">Cloud.ru</a></td>
|
||||
</tr><tr>
|
||||
<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/cloudns/">ClouDNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/cloudxns/">CloudXNS (Deprecated)</a></td>
|
||||
</tr><tr>
|
||||
<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/constellix/">Constellix</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/corenetworks/">Core-Networks</a></td>
|
||||
</tr><tr>
|
||||
<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/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/exoscale/">Exoscale</a></td>
|
||||
</tr><tr>
|
||||
<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>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/freemyip/">freemyip.com</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gcore/">G-Core</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/freemyip/">freemyip.com</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/namesurfer/">FusionLayer NameSurfer</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gcore/">G-Core</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gandi/">Gandi</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gandiv5/">Gandi Live DNS (v5)</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gigahostno/">Gigahost.no</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/glesys/">Glesys</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/godaddy/">Go Daddy</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gcloud/">Google Cloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/googledomains/">Google Domains</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gravity/">Gravity</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/hetzner/">Hetzner</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/hostingde/">Hosting.de</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/hostingnl/">Hosting.nl</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/hostinger/">Hostinger</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/hosttech/">Hosttech</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/httpreq/">HTTP request</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/httpnet/">http.net</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/huaweicloud/">Huawei Cloud</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/hurricane/">Hurricane Electric DNS</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/hyperone/">HyperOne</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ibmcloud/">IBM Cloud (SoftLayer)</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/iijdpf/">IIJ DNS Platform Service</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/infoblox/">Infoblox</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/infomaniak/">Infomaniak</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/iij/">Internet Initiative Japan</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/internetbs/">Internet.bs</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/inwx/">INWX</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ionos/">Ionos</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ionoscloud/">Ionos Cloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ipv64/">IPv64</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ispconfig/">ISPConfig 3</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ispconfigddns/">ISPConfig 3 - Dynamic DNS (DDNS) Module</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/iwantmyname/">iwantmyname (Deprecated)</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/jdcloud/">JD Cloud</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/joker/">Joker</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/acmedns/">Joohoi's ACME-DNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/keyhelp/">KeyHelp</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/leaseweb/">Leaseweb</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/liara/">Liara</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/limacity/">Lima-City</a></td>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package api
|
|||
|
||||
import (
|
||||
"cmp"
|
||||
"maps"
|
||||
"net"
|
||||
"slices"
|
||||
|
||||
|
|
@ -10,7 +9,9 @@ import (
|
|||
)
|
||||
|
||||
func createIdentifiers(domains []string) []acme.Identifier {
|
||||
uniqIdentifiers := make(map[string]acme.Identifier)
|
||||
uniqIdentifiers := make(map[string]struct{})
|
||||
|
||||
var identifiers []acme.Identifier
|
||||
|
||||
for _, domain := range domains {
|
||||
if _, ok := uniqIdentifiers[domain]; ok {
|
||||
|
|
@ -23,10 +24,12 @@ func createIdentifiers(domains []string) []acme.Identifier {
|
|||
ident.Type = "ip"
|
||||
}
|
||||
|
||||
uniqIdentifiers[domain] = ident
|
||||
identifiers = append(identifiers, ident)
|
||||
|
||||
uniqIdentifiers[domain] = struct{}{}
|
||||
}
|
||||
|
||||
return slices.AppendSeq(make([]acme.Identifier, 0, len(uniqIdentifiers)), maps.Values(uniqIdentifiers))
|
||||
return identifiers
|
||||
}
|
||||
|
||||
// compareIdentifiers compares 2 slices of [acme.Identifier].
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ package sender
|
|||
|
||||
const (
|
||||
// ourUserAgent is the User-Agent of this underlying library package.
|
||||
ourUserAgent = "xenolf-acme/4.31.0"
|
||||
ourUserAgent = "xenolf-acme/4.32.0"
|
||||
|
||||
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
|
||||
// values: detach|release
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
|
|
@ -56,3 +59,29 @@ func getRetryAfter(resp *http.Response) string {
|
|||
|
||||
return resp.Header.Get("Retry-After")
|
||||
}
|
||||
|
||||
// ParseRetryAfter parses the Retry-After header value according to RFC 7231.
|
||||
// The header can be either delay-seconds (numeric) or HTTP-date (RFC 1123 format).
|
||||
// https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.3
|
||||
// Returns the duration until the retry time.
|
||||
// TODO(ldez): unexposed this function in v5.
|
||||
func ParseRetryAfter(value string) (time.Duration, error) {
|
||||
if value == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if seconds, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||
return time.Duration(seconds) * time.Second, nil
|
||||
}
|
||||
|
||||
if retryTime, err := time.Parse(time.RFC1123, value); err == nil {
|
||||
duration := time.Until(retryTime)
|
||||
if duration < 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("invalid Retry-After value: %q", value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package api
|
|||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_getLink(t *testing.T) {
|
||||
|
|
@ -53,3 +55,38 @@ func Test_getLink(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRetryAfter(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
value string
|
||||
expected time.Duration
|
||||
}{
|
||||
{
|
||||
desc: "empty header value",
|
||||
value: "",
|
||||
expected: time.Duration(0),
|
||||
},
|
||||
{
|
||||
desc: "delay-seconds",
|
||||
value: "123",
|
||||
expected: 123 * time.Second,
|
||||
},
|
||||
{
|
||||
desc: "HTTP-date",
|
||||
value: time.Now().Add(3 * time.Second).Format(time.RFC1123),
|
||||
expected: 3 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt, err := ParseRetryAfter(test.value)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.InDelta(t, test.expected.Seconds(), rt.Seconds(), 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,18 +29,18 @@ type ProblemDetails struct {
|
|||
}
|
||||
|
||||
func (p *ProblemDetails) Error() string {
|
||||
var msg strings.Builder
|
||||
msg := new(strings.Builder)
|
||||
|
||||
msg.WriteString(fmt.Sprintf("acme: error: %d", p.HTTPStatus))
|
||||
_, _ = fmt.Fprintf(msg, "acme: error: %d", p.HTTPStatus)
|
||||
|
||||
if p.Method != "" || p.URL != "" {
|
||||
msg.WriteString(fmt.Sprintf(" :: %s :: %s", p.Method, p.URL))
|
||||
_, _ = fmt.Fprintf(msg, " :: %s :: %s", p.Method, p.URL)
|
||||
}
|
||||
|
||||
msg.WriteString(fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail))
|
||||
_, _ = fmt.Fprintf(msg, " :: %s :: %s", p.Type, p.Detail)
|
||||
|
||||
for _, sub := range p.SubProblems {
|
||||
msg.WriteString(fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail))
|
||||
_, _ = fmt.Fprintf(msg, ", problem: %q :: %s", sub.Type, sub.Detail)
|
||||
}
|
||||
|
||||
if p.Instance != "" {
|
||||
|
|
|
|||
|
|
@ -220,15 +220,15 @@ func ParsePEMCertificate(cert []byte) (*x509.Certificate, error) {
|
|||
}
|
||||
|
||||
func GetCertificateMainDomain(cert *x509.Certificate) (string, error) {
|
||||
return getMainDomain(cert.Subject, cert.DNSNames)
|
||||
return getMainDomain(cert.Subject, cert.DNSNames, cert.IPAddresses)
|
||||
}
|
||||
|
||||
func GetCSRMainDomain(cert *x509.CertificateRequest) (string, error) {
|
||||
return getMainDomain(cert.Subject, cert.DNSNames)
|
||||
return getMainDomain(cert.Subject, cert.DNSNames, cert.IPAddresses)
|
||||
}
|
||||
|
||||
func getMainDomain(subject pkix.Name, dnsNames []string) (string, error) {
|
||||
if subject.CommonName == "" && len(dnsNames) == 0 {
|
||||
func getMainDomain(subject pkix.Name, dnsNames []string, ips []net.IP) (string, error) {
|
||||
if subject.CommonName == "" && len(dnsNames) == 0 && len(ips) == 0 {
|
||||
return "", errors.New("missing domain")
|
||||
}
|
||||
|
||||
|
|
@ -236,7 +236,11 @@ func getMainDomain(subject pkix.Name, dnsNames []string) (string, error) {
|
|||
return subject.CommonName, nil
|
||||
}
|
||||
|
||||
return dnsNames[0], nil
|
||||
if len(dnsNames) > 0 {
|
||||
return dnsNames[0], nil
|
||||
}
|
||||
|
||||
return ips[0].String(), nil
|
||||
}
|
||||
|
||||
func ExtractDomains(cert *x509.Certificate) []string {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/acme"
|
||||
"github.com/go-acme/lego/v5/acme/api"
|
||||
)
|
||||
|
||||
// RenewalInfoRequest contains the necessary renewal information.
|
||||
|
|
@ -93,9 +94,9 @@ func (c *Certifier) GetRenewalInfo(ctx context.Context, req RenewalInfoRequest)
|
|||
}
|
||||
|
||||
if retry := resp.Header.Get("Retry-After"); retry != "" {
|
||||
info.RetryAfter, err = time.ParseDuration(retry + "s")
|
||||
info.RetryAfter, err = api.ParseRetryAfter(retry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to parse Retry-After header: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,42 @@ func TestCertifier_GetRenewalInfo(t *testing.T) {
|
|||
assert.Equal(t, time.Duration(21600000000000), ri.RetryAfter)
|
||||
}
|
||||
|
||||
func TestCertifier_GetRenewalInfo_retryAfter(t *testing.T) {
|
||||
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
||||
require.NoError(t, err)
|
||||
|
||||
server := tester.MockACMEServer().
|
||||
Route("GET /renewalInfo/"+ariLeafCertID,
|
||||
servermock.RawStringResponse(`{
|
||||
"suggestedWindow": {
|
||||
"start": "2020-03-17T17:51:09Z",
|
||||
"end": "2020-03-17T18:21:09Z"
|
||||
},
|
||||
"explanationUrl": "https://aricapable.ca.example/docs/renewal-advice/"
|
||||
}
|
||||
}`).
|
||||
WithHeader("Content-Type", "application/json").
|
||||
WithHeader("Retry-After", time.Now().UTC().Add(6*time.Hour).Format(time.RFC1123))).
|
||||
BuildHTTPS(t)
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key)
|
||||
require.NoError(t, err)
|
||||
|
||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||
|
||||
ri, err := certifier.GetRenewalInfo(t.Context(), RenewalInfoRequest{leaf})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ri)
|
||||
assert.Equal(t, "2020-03-17T17:51:09Z", ri.SuggestedWindow.Start.Format(time.RFC3339))
|
||||
assert.Equal(t, "2020-03-17T18:21:09Z", ri.SuggestedWindow.End.Format(time.RFC3339))
|
||||
assert.Equal(t, "https://aricapable.ca.example/docs/renewal-advice/", ri.ExplanationURL)
|
||||
|
||||
assert.InDelta(t, 6, ri.RetryAfter.Hours(), 0.001)
|
||||
}
|
||||
|
||||
func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
|
||||
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -100,11 +100,26 @@ func (p *Prober) Solve(ctx context.Context, authorizations []acme.Authorization)
|
|||
}
|
||||
|
||||
func sequentialSolve(ctx context.Context, authSolvers []*selectedAuthSolver, failures obtainError) {
|
||||
// Some CA are using the same token,
|
||||
// this can be a problem with the DNS01 challenge when the DNS provider doesn't support duplicate TXT records.
|
||||
// In the sequential mode, this is not a problem because we can solve the challenges in order.
|
||||
// But it can reduce the number of call the DNS provider APIs.
|
||||
uniq := make(map[string]struct{})
|
||||
|
||||
for i, authSolver := range authSolvers {
|
||||
// Submit the challenge
|
||||
domain := challenge.GetTargetedDomain(authSolver.authz)
|
||||
|
||||
chlg, _ := challenge.FindChallenge(challenge.DNS01, authSolver.authz)
|
||||
|
||||
if solvr, ok := authSolver.solver.(preSolver); ok {
|
||||
if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok && chlg.Token != "" {
|
||||
log.Debug("acme: duplicate token (DNS-01); skipping pre-solve.",
|
||||
slog.String("identifier", authSolver.authz.Identifier.Value))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
err := solvr.PreSolve(ctx, authSolver.authz)
|
||||
if err != nil {
|
||||
failures[domain] = err
|
||||
|
|
@ -113,6 +128,8 @@ func sequentialSolve(ctx context.Context, authSolvers []*selectedAuthSolver, fai
|
|||
|
||||
continue
|
||||
}
|
||||
|
||||
uniq[authSolver.authz.Identifier.Value+chlg.Token] = struct{}{}
|
||||
}
|
||||
|
||||
// Solve the challenge
|
||||
|
|
@ -125,22 +142,46 @@ func sequentialSolve(ctx context.Context, authSolvers []*selectedAuthSolver, fai
|
|||
continue
|
||||
}
|
||||
|
||||
// Clean challenge
|
||||
cleanUp(ctx, authSolver.solver, authSolver.authz)
|
||||
if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok || chlg.Token == "" {
|
||||
// Clean challenge
|
||||
cleanUp(ctx, authSolver.solver, authSolver.authz)
|
||||
|
||||
if len(authSolvers)-1 > i {
|
||||
solvr := authSolver.solver.(sequential)
|
||||
_, interval := solvr.Sequential()
|
||||
log.Info("sequence: wait.", slog.Duration("interval", interval))
|
||||
time.Sleep(interval)
|
||||
if len(authSolvers)-1 > i {
|
||||
solvr := authSolver.solver.(sequential)
|
||||
_, interval := solvr.Sequential()
|
||||
log.Info("sequence: wait.", slog.Duration("interval", interval))
|
||||
time.Sleep(interval)
|
||||
}
|
||||
|
||||
delete(uniq, authSolver.authz.Identifier.Value+chlg.Token)
|
||||
} else {
|
||||
log.Debug("acme: duplicate token (DNS-01); skipping cleanup.",
|
||||
slog.String("identifier", authSolver.authz.Identifier.Value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parallelSolve(ctx context.Context, authSolvers []*selectedAuthSolver, failures obtainError) {
|
||||
// Some CA are using the same token,
|
||||
// this can be a problem with the DNS01 challenge when the DNS provider doesn't support duplicate TXT records.
|
||||
uniq := make(map[string]struct{})
|
||||
|
||||
// For all valid preSolvers, first submit the challenges, so they have max time to propagate
|
||||
for _, authSolver := range authSolvers {
|
||||
authz := authSolver.authz
|
||||
|
||||
chlg, err := challenge.FindChallenge(challenge.DNS01, authz)
|
||||
if err == nil {
|
||||
if _, ok := uniq[authz.Identifier.Value+chlg.Token]; ok {
|
||||
log.Debug("acme: duplicate token (DNS-01); skipping pre-solve.",
|
||||
slog.String("identifier", authSolver.authz.Identifier.Value))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
uniq[authz.Identifier.Value+chlg.Token] = struct{}{}
|
||||
}
|
||||
|
||||
if solvr, ok := authSolver.solver.(preSolver); ok {
|
||||
err := solvr.PreSolve(ctx, authz)
|
||||
if err != nil {
|
||||
|
|
@ -152,6 +193,18 @@ func parallelSolve(ctx context.Context, authSolvers []*selectedAuthSolver, failu
|
|||
defer func() {
|
||||
// Clean all created TXT records
|
||||
for _, authSolver := range authSolvers {
|
||||
chlg, err := challenge.FindChallenge(challenge.DNS01, authSolver.authz)
|
||||
if err == nil {
|
||||
if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok {
|
||||
delete(uniq, authSolver.authz.Identifier.Value+chlg.Token)
|
||||
} else {
|
||||
log.Debug("acme: duplicate token (DNS-01); skipping cleanup.",
|
||||
slog.String("identifier", authSolver.authz.Identifier.Value))
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
cleanUp(ctx, authSolver.solver, authSolver.authz)
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package resolver
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/acme"
|
||||
|
|
@ -12,34 +13,68 @@ type preSolverMock struct {
|
|||
preSolve map[string]error
|
||||
solve map[string]error
|
||||
cleanUp map[string]error
|
||||
|
||||
preSolveCounter int
|
||||
solveCounter int
|
||||
cleanUpCounter int
|
||||
}
|
||||
|
||||
func (s *preSolverMock) PreSolve(ctx context.Context, authorization acme.Authorization) error {
|
||||
s.preSolveCounter++
|
||||
|
||||
return s.preSolve[authorization.Identifier.Value]
|
||||
}
|
||||
|
||||
func (s *preSolverMock) Solve(ctx context.Context, authorization acme.Authorization) error {
|
||||
s.solveCounter++
|
||||
|
||||
return s.solve[authorization.Identifier.Value]
|
||||
}
|
||||
|
||||
func (s *preSolverMock) CleanUp(authorization acme.Authorization) error {
|
||||
func (s *preSolverMock) CleanUp(ctx context.Context, authorization acme.Authorization) error {
|
||||
s.cleanUpCounter++
|
||||
|
||||
return s.cleanUp[authorization.Identifier.Value]
|
||||
}
|
||||
|
||||
func (s *preSolverMock) String() string {
|
||||
return fmt.Sprintf("PreSolve: %d, Solve: %d, CleanUp: %d", s.preSolveCounter, s.solveCounter, s.cleanUpCounter)
|
||||
}
|
||||
|
||||
func createStubAuthorizationHTTP01(domain, status string) acme.Authorization {
|
||||
return createStubAuthorization(domain, status, false, acme.Challenge{
|
||||
Type: challenge.HTTP01.String(),
|
||||
Validated: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func createStubAuthorizationDNS01(domain string, wildcard bool) acme.Authorization {
|
||||
var chlgs []acme.Challenge
|
||||
|
||||
if wildcard {
|
||||
chlgs = append(chlgs, acme.Challenge{
|
||||
Type: challenge.HTTP01.String(),
|
||||
Validated: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
chlgs = append(chlgs, acme.Challenge{
|
||||
Type: challenge.DNS01.String(),
|
||||
Validated: time.Now(),
|
||||
})
|
||||
|
||||
return createStubAuthorization(domain, acme.StatusProcessing, wildcard, chlgs...)
|
||||
}
|
||||
|
||||
func createStubAuthorization(domain, status string, wildcard bool, chlgs ...acme.Challenge) acme.Authorization {
|
||||
return acme.Authorization{
|
||||
Status: status,
|
||||
Expires: time.Now(),
|
||||
Wildcard: wildcard,
|
||||
Status: status,
|
||||
Expires: time.Now(),
|
||||
Identifier: acme.Identifier{
|
||||
Type: challenge.HTTP01.String(),
|
||||
Type: "dns",
|
||||
Value: domain,
|
||||
},
|
||||
Challenges: []acme.Challenge{
|
||||
{
|
||||
Type: challenge.HTTP01.String(),
|
||||
Validated: time.Now(),
|
||||
Error: nil,
|
||||
},
|
||||
},
|
||||
Challenges: chlgs,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,22 @@ package resolver
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v5/acme"
|
||||
"github.com/go-acme/lego/v5/challenge"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProber_Solve(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
solvers map[challenge.Type]solver
|
||||
authz []acme.Authorization
|
||||
expectedError string
|
||||
desc string
|
||||
solvers map[challenge.Type]solver
|
||||
authz []acme.Authorization
|
||||
expectedError string
|
||||
expectedCounters map[challenge.Type]string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
|
|
@ -30,6 +33,30 @@ func TestProber_Solve(t *testing.T) {
|
|||
createStubAuthorizationHTTP01("example.org", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("example.net", acme.StatusProcessing),
|
||||
},
|
||||
expectedCounters: map[challenge.Type]string{
|
||||
challenge.HTTP01: "PreSolve: 3, Solve: 3, CleanUp: 3",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "DNS-01 deduplicate",
|
||||
solvers: map[challenge.Type]solver{
|
||||
challenge.DNS01: &preSolverMock{
|
||||
preSolve: map[string]error{},
|
||||
solve: map[string]error{},
|
||||
cleanUp: map[string]error{},
|
||||
},
|
||||
},
|
||||
authz: []acme.Authorization{
|
||||
createStubAuthorizationDNS01("a.example", false),
|
||||
createStubAuthorizationDNS01("a.example", true),
|
||||
createStubAuthorizationDNS01("b.example", false),
|
||||
createStubAuthorizationDNS01("b.example", true),
|
||||
createStubAuthorizationDNS01("c.example", true),
|
||||
createStubAuthorizationDNS01("d.example", false),
|
||||
},
|
||||
expectedCounters: map[challenge.Type]string{
|
||||
challenge.DNS01: "PreSolve: 4, Solve: 6, CleanUp: 4",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "already valid",
|
||||
|
|
@ -45,6 +72,9 @@ func TestProber_Solve(t *testing.T) {
|
|||
createStubAuthorizationHTTP01("example.org", acme.StatusValid),
|
||||
createStubAuthorizationHTTP01("example.net", acme.StatusValid),
|
||||
},
|
||||
expectedCounters: map[challenge.Type]string{
|
||||
challenge.HTTP01: "PreSolve: 0, Solve: 0, CleanUp: 0",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when preSolve fail, auth is flagged as error and skipped",
|
||||
|
|
@ -69,6 +99,9 @@ func TestProber_Solve(t *testing.T) {
|
|||
expectedError: `error: one or more domains had a problem:
|
||||
[example.com] preSolve error example.com
|
||||
`,
|
||||
expectedCounters: map[challenge.Type]string{
|
||||
challenge.HTTP01: "PreSolve: 3, Solve: 2, CleanUp: 3",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "errors at different stages",
|
||||
|
|
@ -95,6 +128,9 @@ func TestProber_Solve(t *testing.T) {
|
|||
[example.com] preSolve error example.com
|
||||
[example.org] solve error example.org
|
||||
`,
|
||||
expectedCounters: map[challenge.Type]string{
|
||||
challenge.HTTP01: "PreSolve: 3, Solve: 2, CleanUp: 3",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +148,10 @@ func TestProber_Solve(t *testing.T) {
|
|||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
for n, s := range test.solvers {
|
||||
assert.Equal(t, test.expectedCounters[n], fmt.Sprintf("%s", s))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v5"
|
||||
|
|
@ -95,20 +94,18 @@ func validate(ctx context.Context, core *api.Core, domain string, chlg acme.Chal
|
|||
return nil
|
||||
}
|
||||
|
||||
ra, err := strconv.Atoi(chlng.RetryAfter)
|
||||
if err != nil {
|
||||
retryAfter, err := api.ParseRetryAfter(chlng.RetryAfter)
|
||||
if err != nil || retryAfter == 0 {
|
||||
// The ACME server MUST return a Retry-After.
|
||||
// If it doesn't, we'll just poll hard.
|
||||
// If it doesn't, or if it's invalid, we'll just poll hard.
|
||||
// Boulder does not implement the ability to retry challenges or the Retry-After header.
|
||||
// https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md#section-82
|
||||
ra = 5
|
||||
retryAfter = 5 * time.Second
|
||||
}
|
||||
|
||||
initialInterval := time.Duration(ra) * time.Second
|
||||
|
||||
bo := backoff.NewExponentialBackOff()
|
||||
bo.InitialInterval = initialInterval
|
||||
bo.MaxInterval = 10 * initialInterval
|
||||
bo.InitialInterval = retryAfter
|
||||
bo.MaxInterval = 10 * retryAfter
|
||||
|
||||
// After the path is sent, the ACME server will access our server.
|
||||
// Repeatedly check the server for an updated status on our request.
|
||||
|
|
@ -133,7 +130,7 @@ func validate(ctx context.Context, core *api.Core, domain string, chlg acme.Chal
|
|||
|
||||
return wait.Retry(ctx, operation,
|
||||
backoff.WithBackOff(bo),
|
||||
backoff.WithMaxElapsedTime(100*initialInterval))
|
||||
backoff.WithMaxElapsedTime(100*retryAfter))
|
||||
}
|
||||
|
||||
func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) {
|
||||
|
|
|
|||
|
|
@ -463,26 +463,26 @@ func (f FormattableDuration) String() string {
|
|||
seconds := int(d.Seconds()) % 60
|
||||
ns := int(d.Nanoseconds()) % int(time.Second)
|
||||
|
||||
var s strings.Builder
|
||||
s := new(strings.Builder)
|
||||
|
||||
if days > 0 {
|
||||
s.WriteString(fmt.Sprintf("%dd", days))
|
||||
_, _ = fmt.Fprintf(s, "%dd", days)
|
||||
}
|
||||
|
||||
if hours > 0 {
|
||||
s.WriteString(fmt.Sprintf("%dh", hours))
|
||||
_, _ = fmt.Fprintf(s, "%dh", hours)
|
||||
}
|
||||
|
||||
if minutes > 0 {
|
||||
s.WriteString(fmt.Sprintf("%dm", minutes))
|
||||
_, _ = fmt.Fprintf(s, "%dm", minutes)
|
||||
}
|
||||
|
||||
if seconds > 0 {
|
||||
s.WriteString(fmt.Sprintf("%ds", seconds))
|
||||
_, _ = fmt.Fprintf(s, "%ds", seconds)
|
||||
}
|
||||
|
||||
if ns > 0 {
|
||||
s.WriteString(fmt.Sprintf("%dns", ns))
|
||||
_, _ = fmt.Fprintf(s, "%dns", ns)
|
||||
}
|
||||
|
||||
return s.String()
|
||||
|
|
|
|||
97
cmd/zz_gen_cmd_dnshelp.go
generated
97
cmd/zz_gen_cmd_dnshelp.go
generated
|
|
@ -19,6 +19,7 @@ func allDNSCodes() string {
|
|||
"allinkl",
|
||||
"alwaysdata",
|
||||
"anexia",
|
||||
"artfiles",
|
||||
"arvancloud",
|
||||
"auroradns",
|
||||
"autodns",
|
||||
|
|
@ -31,6 +32,7 @@ func allDNSCodes() string {
|
|||
"binarylane",
|
||||
"bindman",
|
||||
"bluecat",
|
||||
"bluecatv2",
|
||||
"bookmyname",
|
||||
"brandit",
|
||||
"bunny",
|
||||
|
|
@ -110,6 +112,7 @@ func allDNSCodes() string {
|
|||
"jdcloud",
|
||||
"joker",
|
||||
"keyhelp",
|
||||
"leaseweb",
|
||||
"liara",
|
||||
"lightsail",
|
||||
"limacity",
|
||||
|
|
@ -130,6 +133,7 @@ func allDNSCodes() string {
|
|||
"namecheap",
|
||||
"namedotcom",
|
||||
"namesilo",
|
||||
"namesurfer",
|
||||
"nearlyfreespeech",
|
||||
"neodigit",
|
||||
"netcup",
|
||||
|
|
@ -262,8 +266,10 @@ func displayDNSHelp(w io.Writer, name string) error {
|
|||
|
||||
ew.writeln(`Additional Configuration:`)
|
||||
ew.writeln(` - "ALICLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`)
|
||||
ew.writeln(` - "ALICLOUD_LINE": Line (Default: default)`)
|
||||
ew.writeln(` - "ALICLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
|
||||
ew.writeln(` - "ALICLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
|
||||
ew.writeln(` - "ALICLOUD_REGION_ID": Region ID (Default: cn-hangzhou)`)
|
||||
ew.writeln(` - "ALICLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)
|
||||
|
||||
ew.writeln()
|
||||
|
|
@ -354,6 +360,27 @@ func displayDNSHelp(w io.Writer, name string) error {
|
|||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/anexia`)
|
||||
|
||||
case "artfiles":
|
||||
// generated from: providers/dns/artfiles/artfiles.toml
|
||||
ew.writeln(`Configuration for ArtFiles.`)
|
||||
ew.writeln(`Code: 'artfiles'`)
|
||||
ew.writeln(`Since: 'v4.32.0'`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Credentials:`)
|
||||
ew.writeln(` - "ARTFILES_PASSWORD": API password`)
|
||||
ew.writeln(` - "ARTFILES_USERNAME": API username`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Additional Configuration:`)
|
||||
ew.writeln(` - "ARTFILES_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
|
||||
ew.writeln(` - "ARTFILES_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
|
||||
ew.writeln(` - "ARTFILES_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 360)`)
|
||||
ew.writeln(` - "ARTFILES_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)
|
||||
|
||||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/artfiles`)
|
||||
|
||||
case "arvancloud":
|
||||
// generated from: providers/dns/arvancloud/arvancloud.toml
|
||||
ew.writeln(`Configuration for ArvanCloud.`)
|
||||
|
|
@ -623,6 +650,31 @@ func displayDNSHelp(w io.Writer, name string) error {
|
|||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/bluecat`)
|
||||
|
||||
case "bluecatv2":
|
||||
// generated from: providers/dns/bluecatv2/bluecatv2.toml
|
||||
ew.writeln(`Configuration for Bluecat v2.`)
|
||||
ew.writeln(`Code: 'bluecatv2'`)
|
||||
ew.writeln(`Since: 'v4.32.0'`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Credentials:`)
|
||||
ew.writeln(` - "BLUECATV2_CONFIG_NAME": Configuration name`)
|
||||
ew.writeln(` - "BLUECATV2_PASSWORD": API password`)
|
||||
ew.writeln(` - "BLUECATV2_USERNAME": API username`)
|
||||
ew.writeln(` - "BLUECATV2_VIEW_NAME": DNS View Name`)
|
||||
ew.writeln(` - "BLUECAT_SERVER_URL": The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Additional Configuration:`)
|
||||
ew.writeln(` - "BLUECATV2_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
|
||||
ew.writeln(` - "BLUECATV2_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
|
||||
ew.writeln(` - "BLUECATV2_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
|
||||
ew.writeln(` - "BLUECATV2_SKIP_DEPLOY": Skip quick deployements`)
|
||||
ew.writeln(` - "BLUECATV2_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)
|
||||
|
||||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/bluecatv2`)
|
||||
|
||||
case "bookmyname":
|
||||
// generated from: providers/dns/bookmyname/bookmyname.toml
|
||||
ew.writeln(`Configuration for BookMyName.`)
|
||||
|
|
@ -2307,6 +2359,26 @@ func displayDNSHelp(w io.Writer, name string) error {
|
|||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/keyhelp`)
|
||||
|
||||
case "leaseweb":
|
||||
// generated from: providers/dns/leaseweb/leaseweb.toml
|
||||
ew.writeln(`Configuration for Leaseweb.`)
|
||||
ew.writeln(`Code: 'leaseweb'`)
|
||||
ew.writeln(`Since: 'v4.32.0'`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Credentials:`)
|
||||
ew.writeln(` - "LEASEWEB_API_KEY": API key`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Additional Configuration:`)
|
||||
ew.writeln(` - "LEASEWEB_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
|
||||
ew.writeln(` - "LEASEWEB_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
|
||||
ew.writeln(` - "LEASEWEB_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
|
||||
ew.writeln(` - "LEASEWEB_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)
|
||||
|
||||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/leaseweb`)
|
||||
|
||||
case "liara":
|
||||
// generated from: providers/dns/liara/liara.toml
|
||||
ew.writeln(`Configuration for Liara.`)
|
||||
|
|
@ -2322,6 +2394,7 @@ func displayDNSHelp(w io.Writer, name string) error {
|
|||
ew.writeln(` - "LIARA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
|
||||
ew.writeln(` - "LIARA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
|
||||
ew.writeln(` - "LIARA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
|
||||
ew.writeln(` - "LIARA_TEAM_ID": The team ID to access services in a team`)
|
||||
ew.writeln(` - "LIARA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)
|
||||
|
||||
ew.writeln()
|
||||
|
|
@ -2714,6 +2787,30 @@ func displayDNSHelp(w io.Writer, name string) error {
|
|||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/namesilo`)
|
||||
|
||||
case "namesurfer":
|
||||
// generated from: providers/dns/namesurfer/namesurfer.toml
|
||||
ew.writeln(`Configuration for FusionLayer NameSurfer.`)
|
||||
ew.writeln(`Code: 'namesurfer'`)
|
||||
ew.writeln(`Since: 'v4.32.0'`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Credentials:`)
|
||||
ew.writeln(` - "NAMESURFER_API_KEY": API key name`)
|
||||
ew.writeln(` - "NAMESURFER_API_SECRET": API secret`)
|
||||
ew.writeln(` - "NAMESURFER_BASE_URL": The base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10)`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Additional Configuration:`)
|
||||
ew.writeln(` - "NAMESURFER_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
|
||||
ew.writeln(` - "NAMESURFER_INSECURE_SKIP_VERIFY": Whether to verify the API certificate`)
|
||||
ew.writeln(` - "NAMESURFER_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
|
||||
ew.writeln(` - "NAMESURFER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`)
|
||||
ew.writeln(` - "NAMESURFER_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)
|
||||
ew.writeln(` - "NAMESURFER_VIEW": DNS view name (optional, default: empty string)`)
|
||||
|
||||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/namesurfer`)
|
||||
|
||||
case "nearlyfreespeech":
|
||||
// generated from: providers/dns/nearlyfreespeech/nearlyfreespeech.toml
|
||||
ew.writeln(`Configuration for NearlyFreeSpeech.NET.`)
|
||||
|
|
|
|||
2
docs/content/dns/zz_gen_alidns.md
generated
2
docs/content/dns/zz_gen_alidns.md
generated
|
|
@ -58,8 +58,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
|||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `ALICLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |
|
||||
| `ALICLOUD_LINE` | Line (Default: default) |
|
||||
| `ALICLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
|
||||
| `ALICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
|
||||
| `ALICLOUD_REGION_ID` | Region ID (Default: cn-hangzhou) |
|
||||
| `ALICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
|
|
|
|||
69
docs/content/dns/zz_gen_artfiles.md
generated
Normal file
69
docs/content/dns/zz_gen_artfiles.md
generated
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
title: "ArtFiles"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: artfiles
|
||||
dnsprovider:
|
||||
since: "v4.32.0"
|
||||
code: "artfiles"
|
||||
url: "https://www.artfiles.de/extras/domains/"
|
||||
---
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/artfiles/artfiles.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
|
||||
|
||||
Configuration for [ArtFiles](https://www.artfiles.de/extras/domains/).
|
||||
|
||||
|
||||
<!--more-->
|
||||
|
||||
- Code: `artfiles`
|
||||
- Since: v4.32.0
|
||||
|
||||
|
||||
Here is an example bash command using the ArtFiles provider:
|
||||
|
||||
```bash
|
||||
ARTFILES_USERNAME="xxx" \
|
||||
ARTFILES_PASSWORD="yyy" \
|
||||
lego --dns artfiles -d '*.example.com' -d example.com run
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Credentials
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `ARTFILES_PASSWORD` | API password |
|
||||
| `ARTFILES_USERNAME` | API username |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `ARTFILES_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
|
||||
| `ARTFILES_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
|
||||
| `ARTFILES_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 360) |
|
||||
| `ARTFILES_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
|
||||
|
||||
## More information
|
||||
|
||||
- [API documentation](https://support.artfiles.de/DCP-API#dns)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/artfiles/artfiles.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
76
docs/content/dns/zz_gen_bluecatv2.md
generated
Normal file
76
docs/content/dns/zz_gen_bluecatv2.md
generated
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
title: "Bluecat v2"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: bluecatv2
|
||||
dnsprovider:
|
||||
since: "v4.32.0"
|
||||
code: "bluecatv2"
|
||||
url: "https://www.bluecatnetworks.com"
|
||||
---
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/bluecatv2/bluecatv2.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
|
||||
|
||||
Configuration for [Bluecat v2](https://www.bluecatnetworks.com).
|
||||
|
||||
|
||||
<!--more-->
|
||||
|
||||
- Code: `bluecatv2`
|
||||
- Since: v4.32.0
|
||||
|
||||
|
||||
Here is an example bash command using the Bluecat v2 provider:
|
||||
|
||||
```bash
|
||||
BLUECATV2_SERVER_URL="https://example.com" \
|
||||
BLUECATV2_USERNAME="xxx" \
|
||||
BLUECATV2_PASSWORD="yyy" \
|
||||
BLUECATV2_CONFIG_NAME="myConfiguration" \
|
||||
BLUECATV2_VIEW_NAME="myView" \
|
||||
lego --dns bluecatv2 -d '*.example.com' -d example.com run
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Credentials
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `BLUECATV2_CONFIG_NAME` | Configuration name |
|
||||
| `BLUECATV2_PASSWORD` | API password |
|
||||
| `BLUECATV2_USERNAME` | API username |
|
||||
| `BLUECATV2_VIEW_NAME` | DNS View Name |
|
||||
| `BLUECAT_SERVER_URL` | The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `BLUECATV2_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
|
||||
| `BLUECATV2_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
|
||||
| `BLUECATV2_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
|
||||
| `BLUECATV2_SKIP_DEPLOY` | Skip quick deployements |
|
||||
| `BLUECATV2_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
|
||||
|
||||
## More information
|
||||
|
||||
- [API documentation](https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Introduction/9.6.0)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/bluecatv2/bluecatv2.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
67
docs/content/dns/zz_gen_leaseweb.md
generated
Normal file
67
docs/content/dns/zz_gen_leaseweb.md
generated
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
title: "Leaseweb"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: leaseweb
|
||||
dnsprovider:
|
||||
since: "v4.32.0"
|
||||
code: "leaseweb"
|
||||
url: "https://www.leaseweb.com/en/"
|
||||
---
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/leaseweb/leaseweb.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
|
||||
|
||||
Configuration for [Leaseweb](https://www.leaseweb.com/en/).
|
||||
|
||||
|
||||
<!--more-->
|
||||
|
||||
- Code: `leaseweb`
|
||||
- Since: v4.32.0
|
||||
|
||||
|
||||
Here is an example bash command using the Leaseweb provider:
|
||||
|
||||
```bash
|
||||
LEASEWEB_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
|
||||
lego --dns leaseweb -d '*.example.com' -d example.com run
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Credentials
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `LEASEWEB_API_KEY` | API key |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `LEASEWEB_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
|
||||
| `LEASEWEB_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
|
||||
| `LEASEWEB_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
|
||||
| `LEASEWEB_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
|
||||
|
||||
## More information
|
||||
|
||||
- [API documentation](https://developer.leaseweb.com/docs/#tag/DNS)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/leaseweb/leaseweb.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.
|
||||
|
|
|
|||
73
docs/content/dns/zz_gen_namesurfer.md
generated
Normal file
73
docs/content/dns/zz_gen_namesurfer.md
generated
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
title: "FusionLayer NameSurfer"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: namesurfer
|
||||
dnsprovider:
|
||||
since: "v4.32.0"
|
||||
code: "namesurfer"
|
||||
url: "https://www.fusionlayer.com/"
|
||||
---
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/namesurfer/namesurfer.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
|
||||
|
||||
Configuration for [FusionLayer NameSurfer](https://www.fusionlayer.com/).
|
||||
|
||||
|
||||
<!--more-->
|
||||
|
||||
- Code: `namesurfer`
|
||||
- Since: v4.32.0
|
||||
|
||||
|
||||
Here is an example bash command using the FusionLayer NameSurfer provider:
|
||||
|
||||
```bash
|
||||
NAMESURFER_BASE_URL=https://foo.example.com:8443/API/NSService_10 \
|
||||
NAMESURFER_API_KEY=xxx \
|
||||
NAMESURFER_API_SECRET=yyy \
|
||||
lego --dns namesurfer -d '*.example.com' -d example.com run
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Credentials
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `NAMESURFER_API_KEY` | API key name |
|
||||
| `NAMESURFER_API_SECRET` | API secret |
|
||||
| `NAMESURFER_BASE_URL` | The base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `NAMESURFER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
|
||||
| `NAMESURFER_INSECURE_SKIP_VERIFY` | Whether to verify the API certificate |
|
||||
| `NAMESURFER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
|
||||
| `NAMESURFER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |
|
||||
| `NAMESURFER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |
|
||||
| `NAMESURFER_VIEW` | DNS view name (optional, default: empty string) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
|
||||
|
||||
## More information
|
||||
|
||||
- [API documentation](https://web.archive.org/web/20260213170737/http://95.128.3.201:8053/API/NSService_10)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/namesurfer/namesurfer.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
2
docs/data/zz_cli_help.toml
generated
2
docs/data/zz_cli_help.toml
generated
|
|
@ -285,7 +285,7 @@ To display the documentation for a specific DNS provider, run:
|
|||
$ lego dnshelp -c code
|
||||
|
||||
Supported DNS providers:
|
||||
acmedns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, googledomains, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, iwantmyname, jdcloud, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, neodigit, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnamesca, webnamesru, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
|
||||
acmedns, 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, webnamesca, webnamesru, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
|
||||
|
||||
More information: https://go-acme.github.io/lego/dns
|
||||
"""
|
||||
|
|
|
|||
109
go.mod
109
go.mod
|
|
@ -5,7 +5,7 @@ go 1.25.0
|
|||
require (
|
||||
cloud.google.com/go/compute/metadata v0.9.0
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0
|
||||
|
|
@ -15,29 +15,29 @@ require (
|
|||
github.com/Azure/go-autorest/autorest/to v0.4.1
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15
|
||||
github.com/alibabacloud-go/tea v1.4.0
|
||||
github.com/aliyun/credentials-go v1.4.7
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.8
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.8
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6
|
||||
github.com/aziontech/azionapi-go-sdk v0.144.0
|
||||
github.com/baidubce/bce-sdk-go v0.9.256
|
||||
github.com/baidubce/bce-sdk-go v0.9.260
|
||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/dnsimple/dnsimple-go/v4 v4.0.0
|
||||
github.com/exoscale/egoscale/v3 v3.1.33
|
||||
github.com/go-acme/alidns-20150109/v4 v4.7.0
|
||||
github.com/go-acme/esa-20240910/v2 v2.44.0
|
||||
github.com/go-acme/esa-20240910/v2 v2.48.0
|
||||
github.com/go-acme/jdcloud-sdk-go v1.64.0
|
||||
github.com/go-acme/tencentclouddnspod v1.1.25
|
||||
github.com/go-acme/tencentedgdeone v1.1.48
|
||||
github.com/go-acme/tencentclouddnspod v1.3.24
|
||||
github.com/go-acme/tencentedgdeone v1.3.38
|
||||
github.com/go-jose/go-jose/v4 v4.1.3
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-querystring v1.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
|
|
@ -45,19 +45,19 @@ require (
|
|||
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||
github.com/hashicorp/go-version v1.8.0
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df
|
||||
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2
|
||||
github.com/ldez/grignotin v0.10.1
|
||||
github.com/linode/linodego v1.64.0
|
||||
github.com/linode/linodego v1.65.0
|
||||
github.com/liquidweb/liquidweb-go v1.6.4
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mattn/go-zglob v0.0.6
|
||||
github.com/miekg/dns v1.1.69
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/mimuret/golang-iij-dpf v0.9.1
|
||||
github.com/namedotcom/go/v4 v4.0.2
|
||||
github.com/nrdcg/auroradns v1.1.0
|
||||
github.com/nrdcg/auroradns v1.2.0
|
||||
github.com/nrdcg/bunny-go v0.1.0
|
||||
github.com/nrdcg/desec v0.11.1
|
||||
github.com/nrdcg/dnspod-go v0.4.0
|
||||
|
|
@ -67,8 +67,8 @@ require (
|
|||
github.com/nrdcg/mailinabox v0.3.0
|
||||
github.com/nrdcg/namesilo v0.5.0
|
||||
github.com/nrdcg/nodion v0.1.0
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2
|
||||
github.com/nrdcg/porkbun v0.4.0
|
||||
github.com/nrdcg/vegadns v0.3.0
|
||||
github.com/nzdjb/go-metaname v1.0.0
|
||||
|
|
@ -82,30 +82,30 @@ require (
|
|||
github.com/selectel/go-selvpcclient/v4 v4.1.0
|
||||
github.com/softlayer/softlayer-go v1.2.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48
|
||||
github.com/transip/gotransip/v6 v6.26.1
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419
|
||||
github.com/urfave/cli/v3 v3.6.2
|
||||
github.com/vinyldns/go-vinyldns v0.9.17
|
||||
github.com/volcengine/volc-sdk-golang v1.0.233
|
||||
github.com/vultr/govultr/v3 v3.26.1
|
||||
github.com/yandex-cloud/go-genproto v0.43.0
|
||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.25
|
||||
github.com/yandex-cloud/go-sdk/v2 v2.37.0
|
||||
github.com/volcengine/volc-sdk-golang v1.0.237
|
||||
github.com/vultr/govultr/v3 v3.27.0
|
||||
github.com/yandex-cloud/go-genproto v0.54.0
|
||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.36
|
||||
github.com/yandex-cloud/go-sdk/v2 v2.56.0
|
||||
gitlab.com/greyxor/slogor v1.6.6
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/text v0.32.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/oauth2 v0.35.0
|
||||
golang.org/x/text v0.34.0
|
||||
golang.org/x/time v0.14.0
|
||||
google.golang.org/api v0.259.0
|
||||
gopkg.in/ns1/ns1-go.v2 v2.16.0
|
||||
google.golang.org/api v0.267.0
|
||||
gopkg.in/ns1/ns1-go.v2 v2.17.2
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
software.sslmate.com/src/go-pkcs12 v0.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.18.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
|
|
@ -121,22 +121,23 @@ require (
|
|||
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
|
|
@ -161,8 +162,8 @@ require (
|
|||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
|
|
@ -204,22 +205,22 @@ require (
|
|||
go.mongodb.org/mongo-driver v1.13.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
233
go.sum
233
go.sum
|
|
@ -13,8 +13,8 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
|
|||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
|
||||
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
|
||||
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
|
|
@ -42,8 +42,8 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs
|
|||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
|
|
@ -121,8 +121,10 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC
|
|||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 h1:Mubp9hXZMTPWZK+WxrR+kKOVFp4Q/PDZrIIM7ByXI9Y=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
|
||||
|
|
@ -169,54 +171,54 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W
|
|||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.8 h1:iu+64gwDKEoKnyTQskSku72dAwggKI5sV6rNvgSMpMs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.8/go.mod h1:MI2XvA+qDi3i9AJxX1E2fu730syEBzp/jnXrjxuHwgI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.8 h1:Jp2JYH1lRT3KhX4mshHPvVYsR5qqRec3hGvEarNYoR0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.8/go.mod h1:fZG9tuvyVfxknv1rKibIz3DobRaFw1Poe8IKtXB3XYY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10 h1:MQuZZ6Tq1qQabPlkVxrCMdyVl70Ogl4AERZKo+y9Wzo=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10/go.mod h1:U5C3JME1ibKESmpzBAqlRpTYZfVbTqrb5ICJm+sVVd8=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 h1:80pDB3Tpmb2RCSZORrK9/3iQxsd+w6vSzVqpT1FGiwE=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0/go.mod h1:6EZUGGNLPLh5Unt30uEoA+KQcByERfXIkax9qrc80nA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 h1:VM5e5M39zRSs+aT0O9SoxHjUXqXxhbw3Yi0FdMQWPIc=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11/go.mod h1:0jvzYPIQGCpnY/dmdaotTk2JH4QuBlnW0oeyrcGLWJ4=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0DINACb1wxM6wdGlx4eHE=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
|
||||
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aziontech/azionapi-go-sdk v0.144.0 h1:T+/w18o+FCiZsk3Z0ACBVVe7c/5EGLG15S3P8JfuPfo=
|
||||
github.com/aziontech/azionapi-go-sdk v0.144.0/go.mod h1:OKxP/R0iVXnJJakYwMhh2BGAXnud8Ruy55Ak9ANuWoU=
|
||||
github.com/baidubce/bce-sdk-go v0.9.256 h1:/6UwBzDp+dRFpKRIb5WsvxfSiG4SLOIOghvagOK/q4Y=
|
||||
github.com/baidubce/bce-sdk-go v0.9.256/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
|
||||
github.com/baidubce/bce-sdk-go v0.9.260 h1:1v1+2GTP+NGK3L24rJ+bnoiTaDaIy2YoaUM+ot2GTcw=
|
||||
github.com/baidubce/bce-sdk-go v0.9.260/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
|
|
@ -241,6 +243,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
|
|||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
|
|
@ -315,14 +319,14 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
|||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-acme/alidns-20150109/v4 v4.7.0 h1:PqJ/wR0JTpL4v0Owu1uM7bPQ1Yww0eQLAuuSdLjjQaQ=
|
||||
github.com/go-acme/alidns-20150109/v4 v4.7.0/go.mod h1:btQvB6xZoN6ykKB74cPhiR+uvhrEE2AFVXm6RDmCHm0=
|
||||
github.com/go-acme/esa-20240910/v2 v2.44.0 h1:ACi2uFb7ig4ousFs/YiFBR+aw3A4SHtOxvkMWB2Hbcs=
|
||||
github.com/go-acme/esa-20240910/v2 v2.44.0/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g=
|
||||
github.com/go-acme/esa-20240910/v2 v2.48.0 h1:muSDyhjDTejxUGe3FTthCPCqRaEdYY9cG3N/AmU52Lc=
|
||||
github.com/go-acme/esa-20240910/v2 v2.48.0/go.mod h1:shPb6hzc1rJL15IJBY8HQ4GZk4E8RC52+52twutEwIg=
|
||||
github.com/go-acme/jdcloud-sdk-go v1.64.0 h1:AW9j5khk8tRYbpBJPxKmqdwIqgLs2Fz3HUK3hn2YXjs=
|
||||
github.com/go-acme/jdcloud-sdk-go v1.64.0/go.mod h1:qc/m8HNX1Zgd7GAv2DSEinup8fwy3Ted3/VVx7LB5bU=
|
||||
github.com/go-acme/tencentclouddnspod v1.1.25 h1:7H3ZKshkaHzCXfRpAHVB5nvxeDDl2XLeNZfrNHiZj/s=
|
||||
github.com/go-acme/tencentclouddnspod v1.1.25/go.mod h1:XXfzp0AYV7UAUsHKT6R0KAUJFhqAUXmWGF07Elpa5cE=
|
||||
github.com/go-acme/tencentedgdeone v1.1.48 h1:WLyLBsRVhSLFmtbEFXk0naLODSQn7X6J0Fc/qR8xVUk=
|
||||
github.com/go-acme/tencentedgdeone v1.1.48/go.mod h1:mu6tA+bPhlSd+CKUfzRikE0mfxmTlBI6dVTn9LY9dRI=
|
||||
github.com/go-acme/tencentclouddnspod v1.3.24 h1:uCSiOW1EJttcnOON+MVVyVDJguFL/Q4NIGkq1CrT9p8=
|
||||
github.com/go-acme/tencentclouddnspod v1.3.24/go.mod h1:RKcB2wSoZncjBA0OEFj59s1ko1XDy+ZsAtk+9uMxUF0=
|
||||
github.com/go-acme/tencentedgdeone v1.3.38 h1:5YsVl0H4A+cwtiUqR1eZbKFdr4OWfYp2KYJopifzKyQ=
|
||||
github.com/go-acme/tencentedgdeone v1.3.38/go.mod h1:yyjTKVmGpMtFv5HqGODqehHnZJ4KWAbG6dAiwWDgCDY=
|
||||
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
|
|
@ -366,8 +370,8 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8Wd
|
|||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
|
||||
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=
|
||||
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
|
||||
|
|
@ -467,12 +471,12 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
|||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||
github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
|
||||
github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw=
|
||||
github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
|
||||
|
|
@ -537,8 +541,8 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
|
|||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182 h1:B3W9acgpqu5XsN8v+W8SOTfqn/6n4JsjgoKBsm30HFY=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 h1:J+U6+eUjIsBhefolFdZW5hQNJbkMj+7msxZrv56Cg2g=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
|
||||
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
|
|
@ -612,8 +616,8 @@ github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufp
|
|||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY=
|
||||
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||
github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
|
||||
github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
|
||||
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
|
||||
github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM=
|
||||
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
|
||||
|
|
@ -651,8 +655,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N
|
|||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34=
|
||||
github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M=
|
||||
github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
|
|
@ -693,8 +697,8 @@ github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1t
|
|||
github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo=
|
||||
github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk=
|
||||
github.com/nrdcg/auroradns v1.2.0 h1:Jg407vTdXZvZKsART9CNWMp8rQOyhBk04q0MsOf0YR4=
|
||||
github.com/nrdcg/auroradns v1.2.0/go.mod h1:hnByA4Z7MOmV4EPRw5eOmEaNRFavcCIz6kONpNxp9LI=
|
||||
github.com/nrdcg/bunny-go v0.1.0 h1:GAHTRpHaG/TxfLZlqoJ8OJFzw8rI74+jOTkzxWh0uHA=
|
||||
github.com/nrdcg/bunny-go v0.1.0/go.mod h1:u+C9dgsspgtWVaAz6QkyV17s9fxD8viwwKoxb9XMz1A=
|
||||
github.com/nrdcg/desec v0.11.1 h1:ilpKmCr4gGsLcyq3RHfHNmlRzm9fzT2XbWxoVaUCS0s=
|
||||
|
|
@ -713,10 +717,10 @@ github.com/nrdcg/namesilo v0.5.0 h1:6QNxT/XxE+f5B+7QlfWorthNzOzcGlBLRQxqi6YeBrE=
|
|||
github.com/nrdcg/namesilo v0.5.0/go.mod h1:4UkwlwQfDt74kSGmhLaDylnBrD94IfflnpoEaj6T2qw=
|
||||
github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw=
|
||||
github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 h1:l0tH15ACQADZAzC+LZ+mo2tIX4H6uZu0ulrVmG5Tqz0=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 h1:gzB4c6ztb38C/jYiqEaFC+mCGcWFHDji9e6jwymY9d4=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2/go.mod h1:l1qIPIq2uRV5WTSvkbhbl/ndbeOu7OCb3UZ+0+2ZSb8=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc=
|
||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||
github.com/nrdcg/vegadns v0.3.0 h1:11FQMw7xVIRUWO9o5+Z/5YZhmPWlm4oxUUH3F6EVqQU=
|
||||
|
|
@ -902,10 +906,10 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
|
|||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.25/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28 h1:Rj1WXXNPm9AsPf0PJhWCvlsqfcKPUYdyVnkmEc3O8sI=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.24/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.38/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 h1:bCs+z6dxRaHWm/C1D/XkSOcCZ0+W2+/6HmIXjpAj+fY=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
|
|
@ -920,22 +924,22 @@ github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
|
|||
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/vinyldns/go-vinyldns v0.9.17 h1:hfPZfCaxcRBX6Gsgl42rLCeoal58/BH8kkvJShzjjdI=
|
||||
github.com/vinyldns/go-vinyldns v0.9.17/go.mod h1:pwWhE9K/leGDOIduVhRGvQ3ecVMHWRfEnKYUTEU3gB4=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.233 h1:Hh2pzwu/Wq19rsZgNo3HdpjQB28D/F0+m6EjLVggmhM=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.233/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
|
||||
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
|
||||
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.237 h1:hpLKiS2BwDcSBtZWSz034foCbd0h3FrHTKlUMqHIdc4=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.237/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
|
||||
github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
|
||||
github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yandex-cloud/go-genproto v0.43.0 h1:HjBesEmCN8ZOhjjh8gs605vvi9/MBJAW3P20OJ4iQnw=
|
||||
github.com/yandex-cloud/go-genproto v0.43.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
|
||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.25 h1:BcGEuOnwq2X3LS2kvFC6BOdZkOq4Lc7XAYvzap/SJJY=
|
||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.25/go.mod h1:B4QHijALUHIjRxL3aqmOwDrHYUI2XdeeG4WKItth3jI=
|
||||
github.com/yandex-cloud/go-sdk/v2 v2.37.0 h1:WvttW6p9xcWag9j+GQv+GJXPggggXGwOlIJNfkWmFWw=
|
||||
github.com/yandex-cloud/go-sdk/v2 v2.37.0/go.mod h1:Dt4a81enjRsm4xMJyW5E1Y/vaUYwXJvUGRdDLuM2k6I=
|
||||
github.com/yandex-cloud/go-genproto v0.54.0 h1:LjEwDPBAtF39HvcPQe8I+ImCnFasCPCOVh2b2Sr2eAg=
|
||||
github.com/yandex-cloud/go-genproto v0.54.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
|
||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.36 h1:sD622+baDvJ2ujhCfoFsCH0XeNsaZNW6loRqvRavjtE=
|
||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.36/go.mod h1:Hh7IKJxULaRzmyM19lQZw+yUDyMM8M3Qrk1LbWqhCkc=
|
||||
github.com/yandex-cloud/go-sdk/v2 v2.56.0 h1:rihPAZbPbHU/BKTLuT64nU1uhbBrO20HhdlLR3Hyoz0=
|
||||
github.com/yandex-cloud/go-sdk/v2 v2.56.0/go.mod h1:jzVBQgamNHoiDsmjog2dPZHMXuGZqmxf/epH+Qb7Emc=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
|
|
@ -967,16 +971,16 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
|
|||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
|
|
@ -1029,8 +1033,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
|||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
|
@ -1074,8 +1078,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
|
@ -1133,16 +1137,16 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
|||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -1246,8 +1250,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
|
@ -1262,8 +1266,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
|||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
@ -1282,8 +1286,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
|
@ -1349,8 +1353,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
@ -1379,8 +1383,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
|
|||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ=
|
||||
google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4=
|
||||
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
|
||||
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
|
|
@ -1419,12 +1423,12 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D
|
|||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
|
|
@ -1473,11 +1477,12 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
|||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
|
||||
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.16.0 h1:mUczKFnrCystSV7yIODzVSbENoud3T7DwstmyVZfqg4=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.16.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.17.2 h1:x8YKHqCJWkC/hddfUhw7FRqTG0x3fr/0ZnWYN+i4THs=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.17.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
const (
|
||||
// ourUserAgent is the User-Agent of this underlying library package.
|
||||
ourUserAgent = "goacme-lego/4.31.0"
|
||||
ourUserAgent = "goacme-lego/4.32.0"
|
||||
|
||||
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
|
||||
// values: detach|release
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const (
|
|||
EnvSecretKey = envNamespace + "SECRET_KEY"
|
||||
EnvSecurityToken = envNamespace + "SECURITY_TOKEN"
|
||||
EnvRegionID = envNamespace + "REGION_ID"
|
||||
EnvLine = envNamespace + "LINE"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
|
|
@ -45,6 +46,7 @@ type Config struct {
|
|||
SecretKey string
|
||||
SecurityToken string
|
||||
RegionID string
|
||||
Line string
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
TTL int
|
||||
|
|
@ -74,6 +76,7 @@ type DNSProvider struct {
|
|||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
config := NewDefaultConfig()
|
||||
config.RegionID = env.GetOrFile(EnvRegionID)
|
||||
config.Line = env.GetOrFile(EnvLine)
|
||||
|
||||
values, err := env.Get(EnvRAMRole)
|
||||
if err == nil {
|
||||
|
|
@ -250,12 +253,18 @@ func (d *DNSProvider) newTxtRecord(zone, fqdn, value string) (*alidns.AddDomainR
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return new(alidns.AddDomainRecordRequest).
|
||||
adrr := new(alidns.AddDomainRecordRequest).
|
||||
SetType("TXT").
|
||||
SetDomainName(zone).
|
||||
SetRR(rr).
|
||||
SetValue(value).
|
||||
SetTTL(int64(d.config.TTL)), nil
|
||||
SetTTL(int64(d.config.TTL))
|
||||
|
||||
if d.config.Line != "" {
|
||||
adrr.SetLine(d.config.Line)
|
||||
}
|
||||
|
||||
return adrr, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) findTxtRecords(ctx context.Context, fqdn string) ([]*alidns.DescribeDomainRecordsResponseBodyDomainRecordsRecord, error) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ lego --dns alidns - -d '*.example.com' -d example.com run
|
|||
ALICLOUD_SECRET_KEY = "Access Key secret"
|
||||
ALICLOUD_SECURITY_TOKEN = "STS Security Token (optional)"
|
||||
[Configuration.Additional]
|
||||
ALICLOUD_REGION_ID = "Region ID (Default: cn-hangzhou)"
|
||||
ALICLOUD_LINE = "Line (Default: default)"
|
||||
ALICLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
|
||||
ALICLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
|
||||
ALICLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)"
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/challenge"
|
||||
"github.com/go-acme/lego/v5/challenge/dns01"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"github.com/go-acme/lego/v5/platform/config/env"
|
||||
"github.com/go-acme/lego/v5/providers/dns/allinkl/internal"
|
||||
"github.com/go-acme/lego/v5/providers/dns/internal/clientdebug"
|
||||
|
|
@ -121,11 +123,6 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
|||
func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.DefaultClient().FindZoneByFqdn(ctx, info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("allinkl: could not find zone for domain %q: %w", domain, err)
|
||||
}
|
||||
|
||||
credential, err := d.identifier.Authentication(ctx, 60, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("allinkl: authentication: %w", err)
|
||||
|
|
@ -133,6 +130,11 @@ func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string
|
|||
|
||||
ctxAuth := internal.WithContext(ctx, credential)
|
||||
|
||||
authZone, err := d.findZone(ctx, info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("allinkl: %w", err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("allinkl: %w", err)
|
||||
|
|
@ -188,3 +190,17 @@ func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (string, error) {
|
||||
for z := range dns01.DomainsSeq(fqdn) {
|
||||
_, errG := d.client.GetDNSSettings(ctx, z, "")
|
||||
if errG != nil {
|
||||
log.Debug("get DNS settings zone", slog.String("zone", z), log.ErrorAttr(errG))
|
||||
continue
|
||||
}
|
||||
|
||||
return z, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unable to find auth zone for '%s'", fqdn)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
package allinkl
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v5/platform/tester"
|
||||
"github.com/go-acme/lego/v5/platform/tester/servermock"
|
||||
"github.com/go-acme/lego/v5/providers/dns/allinkl/internal"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -143,3 +152,108 @@ func TestLiveCleanUp(t *testing.T) {
|
|||
err = provider.CleanUp(t.Context(), envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func mockBuilder() *servermock.Builder[*DNSProvider] {
|
||||
return servermock.NewBuilder(
|
||||
func(server *httptest.Server) (*DNSProvider, error) {
|
||||
config := NewDefaultConfig()
|
||||
config.Login = "user"
|
||||
config.Password = "secret"
|
||||
config.HTTPClient = server.Client()
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.client.BaseURL, _ = url.Parse(server.URL)
|
||||
p.identifier.BaseURL, _ = url.Parse(server.URL)
|
||||
|
||||
return p, err
|
||||
},
|
||||
).Route("POST /KasAuth.php",
|
||||
servermock.ResponseFromInternal("auth.xml"),
|
||||
servermock.CheckRequestBodyFromInternal("auth-request.xml").
|
||||
IgnoreWhitespace(),
|
||||
)
|
||||
}
|
||||
|
||||
func extractKasRequest(reader io.Reader) (*internal.KasRequest, error) {
|
||||
type ReqEnvelope struct {
|
||||
XMLName xml.Name `xml:"Envelope"`
|
||||
Body struct {
|
||||
KasAPI struct {
|
||||
Params string `xml:"Params"`
|
||||
} `xml:"KasApi"`
|
||||
} `xml:"Body"`
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqEnvelope := ReqEnvelope{}
|
||||
|
||||
err = xml.Unmarshal(raw, &reqEnvelope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var kReq internal.KasRequest
|
||||
|
||||
err = json.Unmarshal([]byte(reqEnvelope.Body.KasAPI.Params), &kReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &kReq, nil
|
||||
}
|
||||
|
||||
func TestDNSProvider_Present(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("POST /KasApi.php",
|
||||
http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
kReq, err := extractKasRequest(req.Body)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch kReq.Action {
|
||||
case "get_dns_settings":
|
||||
params := kReq.RequestParams.(map[string]any)
|
||||
|
||||
if params["zone_host"] == "_acme-challenge.example.com." {
|
||||
servermock.ResponseFromInternal("get_dns_settings_not_found.xml").ServeHTTP(rw, req)
|
||||
} else {
|
||||
servermock.ResponseFromInternal("get_dns_settings.xml").ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
case "add_dns_settings":
|
||||
servermock.ResponseFromInternal("add_dns_settings.xml").ServeHTTP(rw, req)
|
||||
|
||||
default:
|
||||
http.Error(rw, fmt.Sprintf("unknown action: %v", kReq.Action), http.StatusBadRequest)
|
||||
}
|
||||
}),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.Present(t.Context(), "example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_CleanUp(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("POST /KasApi.php",
|
||||
servermock.ResponseFromInternal("delete_dns_settings.xml"),
|
||||
servermock.CheckRequestBodyFromInternal("delete_dns_settings-request.xml").
|
||||
IgnoreWhitespace()).
|
||||
Build(t)
|
||||
|
||||
provider.recordIDs["abc"] = "57347450"
|
||||
|
||||
err := provider.CleanUp(t.Context(), "example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,16 +6,21 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v5"
|
||||
"github.com/go-acme/lego/v5/internal/errutils"
|
||||
"github.com/go-acme/lego/v5/platform/wait"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
)
|
||||
|
||||
const apiEndpoint = "https://kasapi.kasserver.com/soap/KasApi.php"
|
||||
const defaultBaseURL = "https://kasapi.kasserver.com/soap/"
|
||||
|
||||
const apiPath = "KasApi.php"
|
||||
|
||||
type Authentication interface {
|
||||
Authentication(ctx context.Context, sessionLifetime int, sessionUpdateLifetime bool) (string, error)
|
||||
|
|
@ -28,16 +33,21 @@ type Client struct {
|
|||
floodTime time.Time
|
||||
muFloodTime sync.Mutex
|
||||
|
||||
baseURL string
|
||||
maxElapsedTime time.Duration
|
||||
|
||||
BaseURL *url.URL
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new Client.
|
||||
func NewClient(login string) *Client {
|
||||
baseURL, _ := url.Parse(defaultBaseURL)
|
||||
|
||||
return &Client{
|
||||
login: login,
|
||||
baseURL: apiEndpoint,
|
||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||
login: login,
|
||||
BaseURL: baseURL,
|
||||
maxElapsedTime: 3 * time.Minute,
|
||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,14 +61,9 @@ func (c *Client) GetDNSSettings(ctx context.Context, zone, recordID string) ([]R
|
|||
requestParams["record_id"] = recordID
|
||||
}
|
||||
|
||||
req, err := c.newRequest(ctx, "get_dns_settings", requestParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var g APIResponse[GetDNSSettingsResponse]
|
||||
|
||||
var g GetDNSSettingsAPIResponse
|
||||
|
||||
err = c.do(req, &g)
|
||||
err := c.doRequest(ctx, "get_dns_settings", requestParams, &g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -70,14 +75,9 @@ func (c *Client) GetDNSSettings(ctx context.Context, zone, recordID string) ([]R
|
|||
|
||||
// AddDNSSettings Creation of a DNS resource record.
|
||||
func (c *Client) AddDNSSettings(ctx context.Context, record DNSRequest) (string, error) {
|
||||
req, err := c.newRequest(ctx, "add_dns_settings", record)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var g APIResponse[AddDNSSettingsResponse]
|
||||
|
||||
var g AddDNSSettingsAPIResponse
|
||||
|
||||
err = c.do(req, &g)
|
||||
err := c.doRequest(ctx, "add_dns_settings", record, &g)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -91,14 +91,9 @@ func (c *Client) AddDNSSettings(ctx context.Context, record DNSRequest) (string,
|
|||
func (c *Client) DeleteDNSSettings(ctx context.Context, recordID string) (string, error) {
|
||||
requestParams := map[string]string{"record_id": recordID}
|
||||
|
||||
req, err := c.newRequest(ctx, "delete_dns_settings", requestParams)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var g APIResponse[DeleteDNSSettingsResponse]
|
||||
|
||||
var g DeleteDNSSettingsAPIResponse
|
||||
|
||||
err = c.do(req, &g)
|
||||
err := c.doRequest(ctx, "delete_dns_settings", requestParams, &g)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -124,7 +119,9 @@ func (c *Client) newRequest(ctx context.Context, action string, requestParams an
|
|||
|
||||
payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAPIEnvelope, body)))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, bytes.NewReader(payload))
|
||||
endpoint := c.BaseURL.JoinPath(apiPath)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
|
@ -132,6 +129,21 @@ func (c *Client) newRequest(ctx context.Context, action string, requestParams an
|
|||
return req, nil
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(ctx context.Context, action string, requestParams, result any) error {
|
||||
return wait.Retry(ctx,
|
||||
func() error {
|
||||
req, err := c.newRequest(ctx, action, requestParams)
|
||||
if err != nil {
|
||||
return backoff.Permanent(err)
|
||||
}
|
||||
|
||||
return c.do(req, result)
|
||||
},
|
||||
backoff.WithBackOff(&backoff.ZeroBackOff{}),
|
||||
backoff.WithMaxElapsedTime(c.maxElapsedTime),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request, result any) error {
|
||||
c.muFloodTime.Lock()
|
||||
time.Sleep(time.Until(c.floodTime))
|
||||
|
|
@ -139,29 +151,40 @@ func (c *Client) do(req *http.Request, result any) error {
|
|||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return errutils.NewHTTPDoError(req, err)
|
||||
return backoff.Permanent(errutils.NewHTTPDoError(req, err))
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
|
||||
return backoff.Permanent(errutils.NewUnexpectedResponseStatusCodeError(req, resp))
|
||||
}
|
||||
|
||||
envlp, err := decodeXML[KasAPIResponseEnvelope](resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
return backoff.Permanent(err)
|
||||
}
|
||||
|
||||
if envlp.Body.Fault != nil {
|
||||
return envlp.Body.Fault
|
||||
if envlp.Body.Fault.Message == "flood_protection" {
|
||||
ft, errP := strconv.ParseFloat(envlp.Body.Fault.Detail, 64)
|
||||
if errP != nil {
|
||||
return fmt.Errorf("parse flood protection delay: %w", envlp.Body.Fault)
|
||||
}
|
||||
|
||||
c.updateFloodTime(ft)
|
||||
|
||||
return envlp.Body.Fault
|
||||
}
|
||||
|
||||
return backoff.Permanent(envlp.Body.Fault)
|
||||
}
|
||||
|
||||
raw := getValue(envlp.Body.KasAPIResponse.Return)
|
||||
|
||||
err = mapstructure.Decode(raw, result)
|
||||
if err != nil {
|
||||
return fmt.Errorf("response struct decode: %w", err)
|
||||
return backoff.Permanent(fmt.Errorf("response struct decode: %w", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package internal
|
|||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -11,15 +13,17 @@ import (
|
|||
|
||||
func setupClient(server *httptest.Server) (*Client, error) {
|
||||
client := NewClient("user")
|
||||
client.baseURL = server.URL
|
||||
client.BaseURL, _ = url.Parse(server.URL)
|
||||
client.HTTPClient = server.Client()
|
||||
|
||||
client.maxElapsedTime = 1 * time.Second
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func TestClient_GetDNSSettings(t *testing.T) {
|
||||
client := servermock.NewBuilder[*Client](setupClient).
|
||||
Route("POST /", servermock.ResponseFromFixture("get_dns_settings.xml"),
|
||||
Route("POST /KasApi.php", servermock.ResponseFromFixture("get_dns_settings.xml"),
|
||||
servermock.CheckRequestBodyFromFixture("get_dns_settings-request.xml").
|
||||
IgnoreWhitespace()).
|
||||
Build(t)
|
||||
|
|
@ -96,9 +100,24 @@ func TestClient_GetDNSSettings(t *testing.T) {
|
|||
assert.Equal(t, expected, records)
|
||||
}
|
||||
|
||||
func TestClient_GetDNSSettings_error_flood_protection(t *testing.T) {
|
||||
client := servermock.NewBuilder[*Client](setupClient).
|
||||
Route("POST /KasApi.php",
|
||||
servermock.ResponseFromFixture("flood_protection.xml"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
assert.Zero(t, client.floodTime)
|
||||
|
||||
_, err := client.GetDNSSettings(mockContext(t), "example.com", "")
|
||||
require.EqualError(t, err, "KasApi: SOAP-ENV:Server: flood_protection: 0.0688529014587")
|
||||
|
||||
assert.NotZero(t, client.floodTime)
|
||||
}
|
||||
|
||||
func TestClient_AddDNSSettings(t *testing.T) {
|
||||
client := servermock.NewBuilder[*Client](setupClient).
|
||||
Route("POST /", servermock.ResponseFromFixture("add_dns_settings.xml"),
|
||||
Route("POST /KasApi.php", servermock.ResponseFromFixture("add_dns_settings.xml"),
|
||||
servermock.CheckRequestBodyFromFixture("add_dns_settings-request.xml").
|
||||
IgnoreWhitespace()).
|
||||
Build(t)
|
||||
|
|
@ -118,7 +137,7 @@ func TestClient_AddDNSSettings(t *testing.T) {
|
|||
|
||||
func TestClient_DeleteDNSSettings(t *testing.T) {
|
||||
client := servermock.NewBuilder[*Client](setupClient).
|
||||
Route("POST /", servermock.ResponseFromFixture("delete_dns_settings.xml"),
|
||||
Route("POST /KasApi.php", servermock.ResponseFromFixture("delete_dns_settings.xml"),
|
||||
servermock.CheckRequestBodyFromFixture("delete_dns_settings-request.xml").
|
||||
IgnoreWhitespace()).
|
||||
Build(t)
|
||||
|
|
|
|||
7
providers/dns/allinkl/internal/fixtures/auth-request.xml
Normal file
7
providers/dns/allinkl/internal/fixtures/auth-request.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<Body>
|
||||
<KasAuth xmlns="https://kasserver.com/">
|
||||
<Params>{"kas_login":"user","kas_auth_data":"secret","kas_auth_type":"plain","session_lifetime":60,"session_update_lifetime":"Y"}</Params>
|
||||
</KasAuth>
|
||||
</Body>
|
||||
</Envelope>
|
||||
11
providers/dns/allinkl/internal/fixtures/flood_protection.xml
Normal file
11
providers/dns/allinkl/internal/fixtures/flood_protection.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<SOAP-ENV:Body>
|
||||
<SOAP-ENV:Fault>
|
||||
<faultcode>SOAP-ENV:Server</faultcode>
|
||||
<faultstring>flood_protection</faultstring>
|
||||
<faultactor>KasApi</faultactor>
|
||||
<detail>0.0688529014587</detail>
|
||||
</SOAP-ENV:Fault>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<SOAP-ENV:Body>
|
||||
<SOAP-ENV:Fault>
|
||||
<faultcode>SOAP-ENV:Server</faultcode>
|
||||
<faultstring>zone_not_found</faultstring>
|
||||
<faultactor>KasApi</faultactor>
|
||||
<detail>example.com</detail>
|
||||
</SOAP-ENV:Fault>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<SOAP-ENV:Body>
|
||||
<SOAP-ENV:Fault>
|
||||
<faultcode>SOAP-ENV:Server</faultcode>
|
||||
<faultstring>zone_syntax_incorrect</faultstring>
|
||||
<faultactor>KasApi</faultactor>
|
||||
<detail>_acme-challenge.example.com</detail>
|
||||
</SOAP-ENV:Fault>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>
|
||||
|
|
@ -6,14 +6,14 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/internal/errutils"
|
||||
)
|
||||
|
||||
// authEndpoint represents the Identity API endpoint to call.
|
||||
const authEndpoint = "https://kasapi.kasserver.com/soap/KasAuth.php"
|
||||
const authPath = "KasAuth.php"
|
||||
|
||||
type token string
|
||||
|
||||
|
|
@ -24,17 +24,19 @@ type Identifier struct {
|
|||
login string
|
||||
password string
|
||||
|
||||
authEndpoint string
|
||||
HTTPClient *http.Client
|
||||
BaseURL *url.URL
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewIdentifier creates a new Identifier.
|
||||
func NewIdentifier(login, password string) *Identifier {
|
||||
baseURL, _ := url.Parse(defaultBaseURL)
|
||||
|
||||
return &Identifier{
|
||||
login: login,
|
||||
password: password,
|
||||
authEndpoint: authEndpoint,
|
||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||
login: login,
|
||||
password: password,
|
||||
BaseURL: baseURL,
|
||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +64,9 @@ func (c *Identifier) Authentication(ctx context.Context, sessionLifetime int, se
|
|||
|
||||
payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAuthEnvelope, body)))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.authEndpoint, bytes.NewReader(payload))
|
||||
endpoint := c.BaseURL.JoinPath(authPath)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package internal
|
|||
import (
|
||||
"context"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v5/platform/tester/servermock"
|
||||
|
|
@ -12,7 +13,7 @@ import (
|
|||
|
||||
func setupIdentifierClient(server *httptest.Server) (*Identifier, error) {
|
||||
client := NewIdentifier("user", "secret")
|
||||
client.authEndpoint = server.URL
|
||||
client.BaseURL, _ = url.Parse(server.URL)
|
||||
client.HTTPClient = server.Client()
|
||||
|
||||
return client, nil
|
||||
|
|
@ -26,10 +27,13 @@ func mockContext(t *testing.T) context.Context {
|
|||
|
||||
func TestIdentifier_Authentication(t *testing.T) {
|
||||
client := servermock.NewBuilder[*Identifier](setupIdentifierClient).
|
||||
Route("POST /", servermock.ResponseFromFixture("auth.xml")).
|
||||
Route("POST /KasAuth.php",
|
||||
servermock.ResponseFromFixture("auth.xml"),
|
||||
servermock.CheckRequestBodyFromFixture("auth-request.xml").
|
||||
IgnoreWhitespace()).
|
||||
Build(t)
|
||||
|
||||
credentialToken, err := client.Authentication(t.Context(), 60, false)
|
||||
credentialToken, err := client.Authentication(t.Context(), 60, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "593959ca04f0de9689b586c6a647d15d", credentialToken)
|
||||
|
|
@ -37,7 +41,7 @@ func TestIdentifier_Authentication(t *testing.T) {
|
|||
|
||||
func TestIdentifier_Authentication_error(t *testing.T) {
|
||||
client := servermock.NewBuilder[*Identifier](setupIdentifierClient).
|
||||
Route("POST /", servermock.ResponseFromFixture("auth_fault.xml")).
|
||||
Route("POST /KasAuth.php", servermock.ResponseFromFixture("auth_fault.xml")).
|
||||
Build(t)
|
||||
|
||||
_, err := client.Authentication(t.Context(), 60, false)
|
||||
|
|
|
|||
|
|
@ -26,10 +26,11 @@ type Fault struct {
|
|||
Code string `xml:"faultcode"`
|
||||
Message string `xml:"faultstring"`
|
||||
Actor string `xml:"faultactor"`
|
||||
Detail string `xml:"detail"`
|
||||
}
|
||||
|
||||
func (f Fault) Error() string {
|
||||
return fmt.Sprintf("%s: %s: %s", f.Actor, f.Code, f.Message)
|
||||
func (f *Fault) Error() string {
|
||||
return fmt.Sprintf("%s: %s: %s: %s", f.Actor, f.Code, f.Message, f.Detail)
|
||||
}
|
||||
|
||||
// KasResponse a KAS SOAP response.
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ type DNSRequest struct {
|
|||
|
||||
// ---
|
||||
|
||||
type GetDNSSettingsAPIResponse struct {
|
||||
Response GetDNSSettingsResponse `json:"Response" mapstructure:"Response"`
|
||||
type APIResponse[T any] struct {
|
||||
Response T `json:"Response" mapstructure:"Response"`
|
||||
}
|
||||
|
||||
type GetDNSSettingsResponse struct {
|
||||
|
|
@ -73,20 +73,12 @@ type ReturnInfo struct {
|
|||
Aux int `json:"record_aux,omitempty" mapstructure:"record_aux"`
|
||||
}
|
||||
|
||||
type AddDNSSettingsAPIResponse struct {
|
||||
Response AddDNSSettingsResponse `json:"Response" mapstructure:"Response"`
|
||||
}
|
||||
|
||||
type AddDNSSettingsResponse struct {
|
||||
KasFloodDelay float64 `json:"KasFloodDelay" mapstructure:"KasFloodDelay"`
|
||||
ReturnInfo string `json:"ReturnInfo" mapstructure:"ReturnInfo"`
|
||||
ReturnString string `json:"ReturnString" mapstructure:"ReturnString"`
|
||||
}
|
||||
|
||||
type DeleteDNSSettingsAPIResponse struct {
|
||||
Response DeleteDNSSettingsResponse `json:"Response"`
|
||||
}
|
||||
|
||||
type DeleteDNSSettingsResponse struct {
|
||||
KasFloodDelay float64 `json:"KasFloodDelay"`
|
||||
ReturnString string `json:"ReturnString"`
|
||||
|
|
|
|||
200
providers/dns/artfiles/artfiles.go
Normal file
200
providers/dns/artfiles/artfiles.go
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
// Package artfiles implements a DNS provider for solving the DNS-01 challenge using ArtFiles.
|
||||
package artfiles
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/challenge/dns01"
|
||||
"github.com/go-acme/lego/v5/platform/config/env"
|
||||
"github.com/go-acme/lego/v5/providers/dns/artfiles/internal"
|
||||
"github.com/go-acme/lego/v5/providers/dns/internal/clientdebug"
|
||||
)
|
||||
|
||||
// Environment variables names.
|
||||
const (
|
||||
envNamespace = "ARTFILES_"
|
||||
|
||||
EnvUsername = envNamespace + "USERNAME"
|
||||
EnvPassword = envNamespace + "PASSWORD"
|
||||
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
// Config is used to configure the creation of the DNSProvider.
|
||||
type Config struct {
|
||||
Username string
|
||||
Password string
|
||||
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 6*time.Minute),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DNSProvider implements the challenge.Provider interface.
|
||||
type DNSProvider struct {
|
||||
config *Config
|
||||
client *internal.Client
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for ArtFiles.
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvUsername, EnvPassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("artfiles: %w", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.Username = values[EnvUsername]
|
||||
config.Password = values[EnvPassword]
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// NewDNSProviderConfig return a DNSProvider instance configured for ArtFiles.
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("artfiles: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
client, err := internal.NewClient(config.Username, config.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("artfiles: %w", err)
|
||||
}
|
||||
|
||||
if config.HTTPClient != nil {
|
||||
client.HTTPClient = config.HTTPClient
|
||||
}
|
||||
|
||||
client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
|
||||
|
||||
return &DNSProvider{
|
||||
config: config,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters.
|
||||
func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
|
||||
|
||||
zone, err := d.findZone(ctx, info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("artfiles: %w", err)
|
||||
}
|
||||
|
||||
records, err := d.client.GetRecords(ctx, zone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("artfiles: get records: %w", err)
|
||||
}
|
||||
|
||||
rv := internal.RecordValue{}
|
||||
|
||||
if len(records["TXT"]) > 0 {
|
||||
var raw string
|
||||
|
||||
err = json.Unmarshal(records["TXT"], &raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("artfiles: unmarshal TXT records: %w", err)
|
||||
}
|
||||
|
||||
rv = internal.ParseRecordValue(raw)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("artfiles: %w", err)
|
||||
}
|
||||
|
||||
rv.Add(subDomain, info.Value)
|
||||
|
||||
err = d.client.SetRecords(ctx, zone, "TXT", rv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("artfiles: set TXT records: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters.
|
||||
func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
|
||||
|
||||
zone, err := d.findZone(ctx, info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("artfiles: %w", err)
|
||||
}
|
||||
|
||||
records, err := d.client.GetRecords(ctx, zone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("artfiles: get records: %w", err)
|
||||
}
|
||||
|
||||
var raw string
|
||||
|
||||
err = json.Unmarshal(records["TXT"], &raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("artfiles: unmarshal TXT records: %w", err)
|
||||
}
|
||||
|
||||
rv := internal.ParseRecordValue(raw)
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("artfiles: %w", err)
|
||||
}
|
||||
|
||||
rv.RemoveValue(subDomain, info.Value)
|
||||
|
||||
err = d.client.SetRecords(ctx, zone, "TXT", rv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("artfiles: set TXT records: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||
// Adjusting here to cope with spikes in propagation times.
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
|
||||
func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (string, error) {
|
||||
domains, err := d.client.GetDomains(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("artfiles: get domains: %w", err)
|
||||
}
|
||||
|
||||
var zone string
|
||||
|
||||
for s := range dns01.UnFqdnDomainsSeq(fqdn) {
|
||||
if slices.Contains(domains, s) {
|
||||
zone = s
|
||||
}
|
||||
}
|
||||
|
||||
if zone == "" {
|
||||
return "", fmt.Errorf("artfiles: could not find the zone for domain %q", fqdn)
|
||||
}
|
||||
|
||||
return zone, nil
|
||||
}
|
||||
24
providers/dns/artfiles/artfiles.toml
Normal file
24
providers/dns/artfiles/artfiles.toml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
Name = "ArtFiles"
|
||||
Description = ''''''
|
||||
URL = "https://www.artfiles.de/extras/domains/"
|
||||
Code = "artfiles"
|
||||
Since = "v4.32.0"
|
||||
|
||||
Example = '''
|
||||
ARTFILES_USERNAME="xxx" \
|
||||
ARTFILES_PASSWORD="yyy" \
|
||||
lego --dns artfiles -d '*.example.com' -d example.com run
|
||||
'''
|
||||
|
||||
[Configuration]
|
||||
[Configuration.Credentials]
|
||||
ARTFILES_USERNAME = "API username"
|
||||
ARTFILES_PASSWORD = "API password"
|
||||
[Configuration.Additional]
|
||||
ARTFILES_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
|
||||
ARTFILES_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 360)"
|
||||
ARTFILES_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
|
||||
ARTFILES_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
|
||||
|
||||
[Links]
|
||||
API = "https://support.artfiles.de/DCP-API#dns"
|
||||
228
providers/dns/artfiles/artfiles_test.go
Normal file
228
providers/dns/artfiles/artfiles_test.go
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
package artfiles
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v5/platform/tester"
|
||||
"github.com/go-acme/lego/v5/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const envDomain = envNamespace + "DOMAIN"
|
||||
|
||||
var envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain)
|
||||
|
||||
func TestNewDNSProvider(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
envVars: map[string]string{
|
||||
EnvUsername: "user",
|
||||
EnvPassword: "secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "missing username",
|
||||
envVars: map[string]string{
|
||||
EnvUsername: "",
|
||||
EnvPassword: "secret",
|
||||
},
|
||||
expected: "artfiles: some credentials information are missing: ARTFILES_USERNAME",
|
||||
},
|
||||
{
|
||||
desc: "missing password",
|
||||
envVars: map[string]string{
|
||||
EnvUsername: "user",
|
||||
EnvPassword: "",
|
||||
},
|
||||
expected: "artfiles: some credentials information are missing: ARTFILES_PASSWORD",
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
envVars: map[string]string{},
|
||||
expected: "artfiles: some credentials information are missing: ARTFILES_USERNAME,ARTFILES_PASSWORD",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
defer envTest.RestoreEnv()
|
||||
|
||||
envTest.ClearEnv()
|
||||
|
||||
envTest.Apply(test.envVars)
|
||||
|
||||
p, err := NewDNSProvider()
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDNSProviderConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
username string
|
||||
password string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
username: "user",
|
||||
password: "secret",
|
||||
},
|
||||
{
|
||||
desc: "missing username",
|
||||
password: "secret",
|
||||
expected: "artfiles: credentials missing",
|
||||
},
|
||||
{
|
||||
desc: "missing Example",
|
||||
username: "user",
|
||||
expected: "artfiles: credentials missing",
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
expected: "artfiles: credentials missing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
config := NewDefaultConfig()
|
||||
config.Username = test.username
|
||||
config.Password = test.password
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLivePresent(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.Present(t.Context(), envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveCleanUp(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.CleanUp(t.Context(), envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func mockBuilder() *servermock.Builder[*DNSProvider] {
|
||||
return servermock.NewBuilder(
|
||||
func(server *httptest.Server) (*DNSProvider, error) {
|
||||
config := NewDefaultConfig()
|
||||
config.Username = "user"
|
||||
config.Password = "secret"
|
||||
config.HTTPClient = server.Client()
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.client.BaseURL, _ = url.Parse(server.URL)
|
||||
|
||||
return p, nil
|
||||
},
|
||||
servermock.CheckHeader().
|
||||
WithBasicAuth("user", "secret"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestDNSProvider_Present(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /domain/get_domains.html",
|
||||
servermock.ResponseFromInternal("domains.txt"),
|
||||
).
|
||||
Route("GET /dns/get_dns.html",
|
||||
servermock.ResponseFromInternal("get_dns.json"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With("domain", "example.com"),
|
||||
).
|
||||
Route("POST /dns/set_dns.html",
|
||||
servermock.ResponseFromInternal("set_dns.json"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With("TXT", `@ "v=spf1 a mx ~all"
|
||||
_acme-challenge "TheAcmeChallenge"
|
||||
_acme-challenge "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"
|
||||
_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf"
|
||||
_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;"
|
||||
_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com"
|
||||
selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff"
|
||||
selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff"`).
|
||||
With("domain", "example.com"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.Present(t.Context(), "example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_CleanUp(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /domain/get_domains.html",
|
||||
servermock.ResponseFromInternal("domains.txt"),
|
||||
).
|
||||
Route("GET /dns/get_dns.html",
|
||||
servermock.ResponseFromInternal("get_dns.json"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With("domain", "example.com"),
|
||||
).
|
||||
Route("POST /dns/set_dns.html",
|
||||
servermock.ResponseFromInternal("set_dns.json"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With("TXT", `@ "v=spf1 a mx ~all"
|
||||
_acme-challenge "TheAcmeChallenge"
|
||||
_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf"
|
||||
_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;"
|
||||
_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com"
|
||||
selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff"
|
||||
selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff"`).
|
||||
With("domain", "example.com"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.CleanUp(t.Context(), "example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
133
providers/dns/artfiles/internal/client.go
Normal file
133
providers/dns/artfiles/internal/client.go
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/internal/errutils"
|
||||
"github.com/go-acme/lego/v5/internal/useragent"
|
||||
)
|
||||
|
||||
const defaultBaseURL = "https://dcp.c.artfiles.de/api/"
|
||||
|
||||
// Client the ArtFiles API client.
|
||||
type Client struct {
|
||||
username string
|
||||
password string
|
||||
|
||||
BaseURL *url.URL
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new Client.
|
||||
func NewClient(username, password string) (*Client, error) {
|
||||
if username == "" || password == "" {
|
||||
return nil, errors.New("credentials missing")
|
||||
}
|
||||
|
||||
baseURL, _ := url.Parse(defaultBaseURL)
|
||||
|
||||
return &Client{
|
||||
username: username,
|
||||
password: password,
|
||||
BaseURL: baseURL,
|
||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetDomains(ctx context.Context) ([]string, error) {
|
||||
endpoint := c.BaseURL.JoinPath("domain", "get_domains.html")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
raw, err := c.do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parseDomains(string(raw))
|
||||
}
|
||||
|
||||
func (c *Client) GetRecords(ctx context.Context, domain string) (map[string]json.RawMessage, error) {
|
||||
endpoint := c.BaseURL.JoinPath("dns", "get_dns.html")
|
||||
|
||||
query := endpoint.Query()
|
||||
query.Set("domain", domain)
|
||||
|
||||
endpoint.RawQuery = query.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
raw, err := c.do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result Records
|
||||
|
||||
err = json.Unmarshal(raw, &result)
|
||||
if err != nil {
|
||||
return nil, errutils.NewUnmarshalError(req, http.StatusOK, raw, err)
|
||||
}
|
||||
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetRecords(ctx context.Context, domain, rType string, value RecordValue) error {
|
||||
endpoint := c.BaseURL.JoinPath("dns", "set_dns.html")
|
||||
|
||||
query := endpoint.Query()
|
||||
query.Set("domain", domain)
|
||||
query.Set(rType, value.String())
|
||||
|
||||
endpoint.RawQuery = query.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
_, err = c.do(req)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request) ([]byte, error) {
|
||||
useragent.SetHeader(req.Header)
|
||||
|
||||
req.SetBasicAuth(c.username, c.password)
|
||||
|
||||
if req.Method == http.MethodPost {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, errutils.NewHTTPDoError(req, err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
|
||||
}
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
return nil, errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
|
||||
}
|
||||
|
||||
return raw, nil
|
||||
}
|
||||
89
providers/dns/artfiles/internal/client_test.go
Normal file
89
providers/dns/artfiles/internal/client_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v5/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func mockBuilder() *servermock.Builder[*Client] {
|
||||
return servermock.NewBuilder[*Client](
|
||||
func(server *httptest.Server) (*Client, error) {
|
||||
client, err := NewClient("user", "secret")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.BaseURL, _ = url.Parse(server.URL)
|
||||
client.HTTPClient = server.Client()
|
||||
|
||||
return client, nil
|
||||
},
|
||||
servermock.CheckHeader().
|
||||
WithBasicAuth("user", "secret"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestClient_GetDomains(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("GET /domain/get_domains.html",
|
||||
servermock.ResponseFromFixture("domains.txt"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
zones, err := client.GetDomains(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := []string{"example.com", "example.org", "example.net"}
|
||||
|
||||
assert.Equal(t, expected, zones)
|
||||
}
|
||||
|
||||
func TestClient_GetRecords(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("GET /dns/get_dns.html",
|
||||
servermock.ResponseFromFixture("get_dns.json"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With("domain", "example.com"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
records, err := client.GetRecords(t.Context(), "example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := map[string]json.RawMessage{
|
||||
"A": json.RawMessage(strconv.Quote("sub1 1.2.3.4\nsub2 1.2.3.4\nsub3 1.2.3.4\nsub4 1.2.3.4\nsub5 1.2.3.4\nsub6 1.2.3.4\nsub7 1.2.3.4\nsub8 1.2.3.4\nsub9 1.2.3.4\nsub10 1.2.3.4\nsub11 1.2.3.4\nsub12 1.2.3.4\nsub13 1.2.3.4\nsub14 1.2.3.4\nsub15 1.2.3.4\nsub16 1.2.3.4\nsub17 1.2.3.4\nsub18 1.2.3.4\n@ 1.2.3.4")),
|
||||
"AAAA": json.RawMessage(strconv.Quote("")),
|
||||
"CAA": json.RawMessage(strconv.Quote("@ 128 iodef \"mailto:someone@example.tld\"\n@ 128 issue \"letsencrypt.org\"\n@ 128 issuewild \"letsencrypt.org\"")),
|
||||
"CName": json.RawMessage(strconv.Quote("some cname.to.example.tld.")),
|
||||
"MX": json.RawMessage(strconv.Quote("10 mail.example.tld.")),
|
||||
"SRV": json.RawMessage(strconv.Quote("_imap._tcp 0 0 0 .\n_imaps._tcp 0 1 993 mail.example.tld.\n_pop3._tcp 0 0 0 .\n_pop3s._tcp 0 0 0 .")),
|
||||
"TLSA": json.RawMessage(strconv.Quote("_25._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_25._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_25._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_465._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_465._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_465._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_587._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_587._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_587._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2")),
|
||||
"TXT": json.RawMessage(strconv.Quote("_dmarc \"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\"\n_mta-sts \"v=STSv1;id=yyyymmddTHHMMSS;\"\n_smtp._tls \"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\"\n@ \"v=spf1 a mx ~all\"\nselector._domainkey \"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\"\nselectorecc._domainkey \"v=DKIM1;k=ed25519;p=Base64Stuff\"\n_acme-challenge \"TheAcmeChallenge\"")),
|
||||
"TTL": json.RawMessage("3600"),
|
||||
"comment": json.RawMessage(strconv.Quote("TLSA RR:\nInfo -> https://dnssec-stats.ant.isi.edu/~viktor/x3hosts.html\nTest 1 -> https://stats.dnssec-tools.org/explore/?example.tld\nTest 2 -> https://dane.sys4.de/smtp/example.tld\n\nSMIMEA RR:\nGenerator -> https://www.smimea.info/smimea-generator.php\nTest -> https://www.smimea.info/smimea-test.php")),
|
||||
"nameserver": json.RawMessage(strconv.Quote("auth1.artfiles.de.\nauth2.artfiles.de.")),
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, records)
|
||||
}
|
||||
|
||||
func TestClient_SetRecords(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("POST /dns/set_dns.html",
|
||||
servermock.ResponseFromFixture("set_dns.json"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With("TXT", "a b\nc \"d\"").
|
||||
With("domain", "example.com"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := client.SetRecords(t.Context(), "example.com", "TXT", RecordValue{"c": []string{`"d"`}, "a": []string{"b"}})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
3
providers/dns/artfiles/internal/fixtures/domains.txt
Normal file
3
providers/dns/artfiles/internal/fixtures/domains.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
example.com normal 2026-10-01 2017-09-18 163477
|
||||
example.org normal 2026-08-01 2016-07-07 156216
|
||||
example.net normal 2026-07-01 2017-06-06 162462
|
||||
16
providers/dns/artfiles/internal/fixtures/get_dns.json
Normal file
16
providers/dns/artfiles/internal/fixtures/get_dns.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"data": {
|
||||
"SRV": "_imap._tcp 0 0 0 .\n_imaps._tcp 0 1 993 mail.example.tld.\n_pop3._tcp 0 0 0 .\n_pop3s._tcp 0 0 0 .",
|
||||
"AAAA": "",
|
||||
"MX": "10 mail.example.tld.",
|
||||
"CAA": "@ 128 iodef \"mailto:someone@example.tld\"\n@ 128 issue \"letsencrypt.org\"\n@ 128 issuewild \"letsencrypt.org\"",
|
||||
"TTL": 3600,
|
||||
"comment": "TLSA RR:\nInfo -> https://dnssec-stats.ant.isi.edu/~viktor/x3hosts.html\nTest 1 -> https://stats.dnssec-tools.org/explore/?example.tld\nTest 2 -> https://dane.sys4.de/smtp/example.tld\n\nSMIMEA RR:\nGenerator -> https://www.smimea.info/smimea-generator.php\nTest -> https://www.smimea.info/smimea-test.php",
|
||||
"TXT": "_dmarc \"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\"\n_mta-sts \"v=STSv1;id=yyyymmddTHHMMSS;\"\n_smtp._tls \"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\"\n@ \"v=spf1 a mx ~all\"\nselector._domainkey \"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\"\nselectorecc._domainkey \"v=DKIM1;k=ed25519;p=Base64Stuff\"\n_acme-challenge \"TheAcmeChallenge\"",
|
||||
"A": "sub1 1.2.3.4\nsub2 1.2.3.4\nsub3 1.2.3.4\nsub4 1.2.3.4\nsub5 1.2.3.4\nsub6 1.2.3.4\nsub7 1.2.3.4\nsub8 1.2.3.4\nsub9 1.2.3.4\nsub10 1.2.3.4\nsub11 1.2.3.4\nsub12 1.2.3.4\nsub13 1.2.3.4\nsub14 1.2.3.4\nsub15 1.2.3.4\nsub16 1.2.3.4\nsub17 1.2.3.4\nsub18 1.2.3.4\n@ 1.2.3.4",
|
||||
"nameserver": "auth1.artfiles.de.\nauth2.artfiles.de.",
|
||||
"CName": "some cname.to.example.tld.",
|
||||
"TLSA": "_25._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_25._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_25._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_465._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_465._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_465._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_587._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_587._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_587._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2"
|
||||
},
|
||||
"status": "OK"
|
||||
}
|
||||
4
providers/dns/artfiles/internal/fixtures/set_dns.json
Normal file
4
providers/dns/artfiles/internal/fixtures/set_dns.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"status": "OK",
|
||||
"error": ""
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf"
|
||||
_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;"
|
||||
_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com"
|
||||
@ "v=spf1 a mx ~all"
|
||||
selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff"
|
||||
selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff"
|
||||
_acme-challenge "xxx"
|
||||
_acme-challenge "yyy"
|
||||
7
providers/dns/artfiles/internal/fixtures/txt_record.txt
Normal file
7
providers/dns/artfiles/internal/fixtures/txt_record.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf"
|
||||
_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;"
|
||||
_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com"
|
||||
@ "v=spf1 a mx ~all"
|
||||
selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff"
|
||||
selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff"
|
||||
_acme-challenge "TheAcmeChallenge"
|
||||
109
providers/dns/artfiles/internal/types.go
Normal file
109
providers/dns/artfiles/internal/types.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"maps"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Records struct {
|
||||
Data map[string]json.RawMessage `json:"data"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type RecordValue map[string][]string
|
||||
|
||||
func (r RecordValue) Set(key, value string) {
|
||||
r[key] = []string{strconv.Quote(value)}
|
||||
}
|
||||
|
||||
func (r RecordValue) Add(key, value string) {
|
||||
r[key] = append(r[key], strconv.Quote(value))
|
||||
}
|
||||
|
||||
func (r RecordValue) Delete(key string) {
|
||||
delete(r, key)
|
||||
}
|
||||
|
||||
func (r RecordValue) RemoveValue(key, value string) {
|
||||
if len(r[key]) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
quotedValue := strconv.Quote(value)
|
||||
|
||||
var data []string
|
||||
|
||||
for _, s := range r[key] {
|
||||
if s != quotedValue {
|
||||
data = append(data, s)
|
||||
}
|
||||
}
|
||||
|
||||
r[key] = data
|
||||
|
||||
if len(r[key]) == 0 {
|
||||
r.Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
func (r RecordValue) String() string {
|
||||
var parts []string
|
||||
|
||||
for _, key := range slices.Sorted(maps.Keys(r)) {
|
||||
for _, s := range r[key] {
|
||||
parts = append(parts, key+" "+s)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
func ParseRecordValue(lines string) RecordValue {
|
||||
data := make(RecordValue)
|
||||
|
||||
for line := range strings.Lines(lines) {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
idx := strings.IndexFunc(line, unicode.IsSpace)
|
||||
|
||||
data[line[:idx]] = append(data[line[:idx]], line[idx+1:])
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func parseDomains(input string) ([]string, error) {
|
||||
reader := csv.NewReader(strings.NewReader(input))
|
||||
reader.Comma = '\t'
|
||||
reader.TrimLeadingSpace = true
|
||||
reader.LazyQuotes = true
|
||||
|
||||
var data []string
|
||||
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(record) < 1 {
|
||||
// Malformed line
|
||||
continue
|
||||
}
|
||||
|
||||
data = append(data, record[0])
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
183
providers/dns/artfiles/internal/types_test.go
Normal file
183
providers/dns/artfiles/internal/types_test.go
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRecordValue_Set(t *testing.T) {
|
||||
rv := make(RecordValue)
|
||||
|
||||
rv.Set("a", "1")
|
||||
rv.Set("b", "2")
|
||||
rv.Set("b", "3")
|
||||
|
||||
assert.Equal(t, "a \"1\"\nb \"3\"", rv.String())
|
||||
}
|
||||
|
||||
func TestRecordValue_Add(t *testing.T) {
|
||||
rv := make(RecordValue)
|
||||
|
||||
rv.Add("a", "1")
|
||||
rv.Add("b", "2")
|
||||
rv.Add("b", "3")
|
||||
|
||||
assert.Equal(t, "a \"1\"\nb \"2\"\nb \"3\"", rv.String())
|
||||
}
|
||||
|
||||
func TestRecordValue_Delete(t *testing.T) {
|
||||
rv := make(RecordValue)
|
||||
|
||||
rv.Set("a", "1")
|
||||
rv.Add("b", "2")
|
||||
|
||||
rv.Delete("b")
|
||||
|
||||
assert.Equal(t, "a \"1\"", rv.String())
|
||||
}
|
||||
|
||||
func TestRecordValue_RemoveValue(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
data map[string][]string
|
||||
toRemove map[string][]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "remove the only value",
|
||||
data: map[string][]string{
|
||||
"a": {"1"},
|
||||
},
|
||||
toRemove: map[string][]string{
|
||||
"a": {"1"},
|
||||
},
|
||||
expected: ``,
|
||||
},
|
||||
{
|
||||
desc: "remove value in the middle",
|
||||
data: map[string][]string{
|
||||
"a": {"1", "2", "3"},
|
||||
},
|
||||
toRemove: map[string][]string{
|
||||
"a": {"2"},
|
||||
},
|
||||
expected: "a \"1\"\na \"3\"",
|
||||
},
|
||||
{
|
||||
desc: "remove value at the beginning",
|
||||
data: map[string][]string{
|
||||
"a": {"1", "2", "3"},
|
||||
},
|
||||
toRemove: map[string][]string{
|
||||
"a": {"1"},
|
||||
},
|
||||
expected: "a \"2\"\na \"3\"",
|
||||
},
|
||||
{
|
||||
desc: "remove value at the end",
|
||||
data: map[string][]string{
|
||||
"a": {"1", "2", "3"},
|
||||
},
|
||||
toRemove: map[string][]string{
|
||||
"a": {"3"},
|
||||
},
|
||||
expected: "a \"1\"\na \"2\"",
|
||||
},
|
||||
{
|
||||
desc: "remove all (delete)",
|
||||
data: map[string][]string{
|
||||
"a": {"1", "2", "3"},
|
||||
},
|
||||
toRemove: map[string][]string{
|
||||
"a": {"1", "2", "3"},
|
||||
},
|
||||
expected: ``,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rv := make(RecordValue)
|
||||
|
||||
for k, values := range test.data {
|
||||
for _, v := range values {
|
||||
rv.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
for k, values := range test.toRemove {
|
||||
for _, v := range values {
|
||||
rv.RemoveValue(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, test.expected, rv.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRecordValue(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
filename string
|
||||
expected RecordValue
|
||||
}{
|
||||
{
|
||||
desc: "simple",
|
||||
filename: "txt_record.txt",
|
||||
expected: RecordValue{
|
||||
"@": []string{"\"v=spf1 a mx ~all\""},
|
||||
"_acme-challenge": []string{"\"TheAcmeChallenge\""},
|
||||
"_dmarc": []string{"\"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\""},
|
||||
"_mta-sts": []string{"\"v=STSv1;id=yyyymmddTHHMMSS;\""},
|
||||
"_smtp._tls": []string{"\"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\""},
|
||||
"selector._domainkey": []string{"\"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\""},
|
||||
"selectorecc._domainkey": []string{"\"v=DKIM1;k=ed25519;p=Base64Stuff\""},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple values with the same key",
|
||||
filename: "txt_record-multiple.txt",
|
||||
expected: RecordValue{
|
||||
"@": []string{"\"v=spf1 a mx ~all\""},
|
||||
"_acme-challenge": []string{"\"xxx\"", "\"yyy\""},
|
||||
"_dmarc": []string{"\"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\""},
|
||||
"_mta-sts": []string{"\"v=STSv1;id=yyyymmddTHHMMSS;\""},
|
||||
"_smtp._tls": []string{"\"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\""},
|
||||
"selector._domainkey": []string{"\"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\""},
|
||||
"selectorecc._domainkey": []string{"\"v=DKIM1;k=ed25519;p=Base64Stuff\""},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file, err := os.ReadFile(filepath.Join("fixtures", test.filename))
|
||||
require.NoError(t, err)
|
||||
|
||||
data := ParseRecordValue(string(file))
|
||||
|
||||
assert.Equal(t, test.expected, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseDomains(t *testing.T) {
|
||||
file, err := os.ReadFile(filepath.FromSlash("./fixtures/domains.txt"))
|
||||
require.NoError(t, err)
|
||||
|
||||
domains, err := parseDomains(string(file))
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := []string{"example.com", "example.org", "example.net"}
|
||||
|
||||
assert.Equal(t, expected, domains)
|
||||
}
|
||||
|
|
@ -15,12 +15,12 @@ type APIError struct {
|
|||
}
|
||||
|
||||
func (a *APIError) Error() string {
|
||||
var msg strings.Builder
|
||||
msg := new(strings.Builder)
|
||||
|
||||
msg.WriteString(fmt.Sprintf("%d: %s: %s: %s: %s", a.Status, a.Type, a.Title, a.Detail, a.Instance))
|
||||
_, _ = fmt.Fprintf(msg, "%d: %s: %s: %s: %s", a.Status, a.Type, a.Title, a.Detail, a.Instance)
|
||||
|
||||
for s, values := range a.Errors {
|
||||
msg.WriteString(fmt.Sprintf(": %s: %s", s, strings.Join(values, ", ")))
|
||||
_, _ = fmt.Fprintf(msg, ": %s: %s", s, strings.Join(values, ", "))
|
||||
}
|
||||
|
||||
return msg.String()
|
||||
|
|
|
|||
249
providers/dns/bluecatv2/bluecatv2.go
Normal file
249
providers/dns/bluecatv2/bluecatv2.go
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
// Package bluecatv2 implements a DNS provider for solving the DNS-01 challenge using Bluecat v2.
|
||||
package bluecatv2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/challenge/dns01"
|
||||
"github.com/go-acme/lego/v5/platform/config/env"
|
||||
"github.com/go-acme/lego/v5/providers/dns/bluecatv2/internal"
|
||||
"github.com/go-acme/lego/v5/providers/dns/internal/clientdebug"
|
||||
)
|
||||
|
||||
// Environment variables names.
|
||||
const (
|
||||
envNamespace = "BLUECATV2_"
|
||||
|
||||
EnvServerURL = envNamespace + "SERVER_URL"
|
||||
EnvUsername = envNamespace + "USERNAME"
|
||||
EnvPassword = envNamespace + "PASSWORD"
|
||||
EnvConfigName = envNamespace + "CONFIG_NAME"
|
||||
EnvViewName = envNamespace + "VIEW_NAME"
|
||||
EnvSkipDeploy = envNamespace + "SKIP_DEPLOY"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
// Config is used to configure the creation of the DNSProvider.
|
||||
type Config struct {
|
||||
ServerURL string
|
||||
Username string
|
||||
Password string
|
||||
ConfigName string
|
||||
ViewName string
|
||||
SkipDeploy bool
|
||||
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
TTL int
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
SkipDeploy: env.GetOrDefaultBool(EnvSkipDeploy, false),
|
||||
|
||||
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DNSProvider implements the challenge.Provider interface.
|
||||
type DNSProvider struct {
|
||||
config *Config
|
||||
client *internal.Client
|
||||
|
||||
zoneIDs map[string]int64
|
||||
recordIDs map[string]int64
|
||||
recordIDsMu sync.Mutex
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for Bluecat v2.
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvServerURL, EnvUsername, EnvPassword, EnvConfigName, EnvViewName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bluecatv2: %w", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.ServerURL = values[EnvServerURL]
|
||||
config.Username = values[EnvUsername]
|
||||
config.Password = values[EnvPassword]
|
||||
config.ConfigName = values[EnvConfigName]
|
||||
config.ViewName = values[EnvViewName]
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// NewDNSProviderConfig return a DNSProvider instance configured for Bluecat v2.
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("bluecatv2: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
if config.ServerURL == "" {
|
||||
return nil, errors.New("bluecatv2: missing server URL")
|
||||
}
|
||||
|
||||
if config.ConfigName == "" {
|
||||
return nil, errors.New("bluecatv2: missing configuration name")
|
||||
}
|
||||
|
||||
if config.ViewName == "" {
|
||||
return nil, errors.New("bluecatv2: missing view name")
|
||||
}
|
||||
|
||||
client, err := internal.NewClient(config.ServerURL, config.Username, config.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bluecatv2: %w", err)
|
||||
}
|
||||
|
||||
if config.HTTPClient != nil {
|
||||
client.HTTPClient = config.HTTPClient
|
||||
}
|
||||
|
||||
client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
|
||||
|
||||
return &DNSProvider{
|
||||
config: config,
|
||||
client: client,
|
||||
recordIDs: make(map[string]int64),
|
||||
zoneIDs: make(map[string]int64),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters.
|
||||
func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
|
||||
|
||||
ctxAuth, err := d.client.CreateAuthenticatedContext(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("bluecatv2: %w", err)
|
||||
}
|
||||
|
||||
zone, err := d.findZone(ctxAuth, info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bluecatv2: %w", err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.AbsoluteName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bluecatv2: %w", err)
|
||||
}
|
||||
|
||||
record := internal.RecordTXT{
|
||||
CommonResource: internal.CommonResource{
|
||||
Type: "TXTRecord",
|
||||
Name: subDomain,
|
||||
},
|
||||
Text: info.Value,
|
||||
TTL: d.config.TTL,
|
||||
RecordType: "TXT",
|
||||
}
|
||||
|
||||
newRecord, err := d.client.CreateZoneResourceRecord(ctxAuth, zone.ID, record)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bluecatv2: create resource record: %w", err)
|
||||
}
|
||||
|
||||
d.recordIDsMu.Lock()
|
||||
d.zoneIDs[token] = zone.ID
|
||||
d.recordIDs[token] = newRecord.ID
|
||||
d.recordIDsMu.Unlock()
|
||||
|
||||
if d.config.SkipDeploy {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = d.client.CreateZoneDeployment(ctxAuth, zone.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bluecat: deploy zone: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters.
|
||||
func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
|
||||
|
||||
d.recordIDsMu.Lock()
|
||||
recordID, recordOK := d.recordIDs[token]
|
||||
zoneID, zoneOK := d.zoneIDs[token]
|
||||
d.recordIDsMu.Unlock()
|
||||
|
||||
if !recordOK {
|
||||
return fmt.Errorf("bluecatv2: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token)
|
||||
}
|
||||
|
||||
if !zoneOK {
|
||||
return fmt.Errorf("bluecatv2: unknown zone ID for '%s' '%s'", info.EffectiveFQDN, token)
|
||||
}
|
||||
|
||||
ctxAuth, err := d.client.CreateAuthenticatedContext(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("bluecatv2: %w", err)
|
||||
}
|
||||
|
||||
err = d.client.DeleteResourceRecord(ctxAuth, recordID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bluecatv2: delete resource record: %w", err)
|
||||
}
|
||||
|
||||
if d.config.SkipDeploy {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = d.client.CreateZoneDeployment(ctxAuth, zoneID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bluecat: deploy zone: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||
// Adjusting here to cope with spikes in propagation times.
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
|
||||
func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.ZoneResource, error) {
|
||||
for name := range dns01.UnFqdnDomainsSeq(fqdn) {
|
||||
opts := &internal.CollectionOptions{
|
||||
Fields: "id,absoluteName,configuration.id,configuration.name,view.id,view.name",
|
||||
Filter: internal.And(
|
||||
internal.Eq("absoluteName", name),
|
||||
internal.Eq("configuration.name", d.config.ConfigName),
|
||||
internal.Eq("view.name", d.config.ViewName),
|
||||
).String(),
|
||||
}
|
||||
|
||||
zones, err := d.client.RetrieveZones(ctx, opts)
|
||||
if err != nil {
|
||||
// TODO(ldez) maybe add a log in v5.
|
||||
continue
|
||||
}
|
||||
|
||||
for _, zone := range zones {
|
||||
if zone.AbsoluteName == name {
|
||||
return &zone, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no zone found for fqdn: %s", fqdn)
|
||||
}
|
||||
33
providers/dns/bluecatv2/bluecatv2.toml
Normal file
33
providers/dns/bluecatv2/bluecatv2.toml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
Name = "Bluecat v2"
|
||||
Description = ''''''
|
||||
URL = "https://www.bluecatnetworks.com"
|
||||
Code = "bluecatv2"
|
||||
Since = "v4.32.0"
|
||||
|
||||
Example = '''
|
||||
BLUECATV2_SERVER_URL="https://example.com" \
|
||||
BLUECATV2_USERNAME="xxx" \
|
||||
BLUECATV2_PASSWORD="yyy" \
|
||||
BLUECATV2_CONFIG_NAME="myConfiguration" \
|
||||
BLUECATV2_VIEW_NAME="myView" \
|
||||
lego --dns bluecatv2 -d '*.example.com' -d example.com run
|
||||
'''
|
||||
|
||||
[Configuration]
|
||||
[Configuration.Credentials]
|
||||
BLUECAT_SERVER_URL = "The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve"
|
||||
BLUECATV2_USERNAME = "API username"
|
||||
BLUECATV2_PASSWORD = "API password"
|
||||
BLUECATV2_CONFIG_NAME = "Configuration name"
|
||||
BLUECATV2_VIEW_NAME = "DNS View Name"
|
||||
[Configuration.Additional]
|
||||
BLUECATV2_SKIP_DEPLOY = "Skip quick deployements"
|
||||
BLUECATV2_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
|
||||
BLUECATV2_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
|
||||
BLUECATV2_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
|
||||
BLUECATV2_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
|
||||
|
||||
[Links]
|
||||
API = "https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Introduction/9.6.0"
|
||||
Swagger = "http://{Address_Manager_IP}/api/openapi.json"
|
||||
SwaggerDump = "https://github.com/go-acme/lego/discussions/2218#discussioncomment-13060545"
|
||||
414
providers/dns/bluecatv2/bluecatv2_test.go
Normal file
414
providers/dns/bluecatv2/bluecatv2_test.go
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
package bluecatv2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v5/platform/tester"
|
||||
"github.com/go-acme/lego/v5/platform/tester/servermock"
|
||||
"github.com/go-acme/lego/v5/providers/dns/bluecatv2/internal"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const envDomain = envNamespace + "DOMAIN"
|
||||
|
||||
var envTest = tester.NewEnvTest(
|
||||
EnvServerURL,
|
||||
EnvUsername,
|
||||
EnvPassword,
|
||||
EnvConfigName,
|
||||
EnvViewName,
|
||||
EnvSkipDeploy,
|
||||
).WithDomain(envDomain)
|
||||
|
||||
func TestNewDNSProvider(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
envVars: map[string]string{
|
||||
EnvServerURL: "https://example.com/",
|
||||
EnvUsername: "userA",
|
||||
EnvPassword: "secret",
|
||||
EnvConfigName: "myConfig",
|
||||
EnvViewName: "myView",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "missing server URL",
|
||||
envVars: map[string]string{
|
||||
EnvServerURL: "",
|
||||
EnvUsername: "userA",
|
||||
EnvPassword: "secret",
|
||||
EnvConfigName: "myConfig",
|
||||
EnvViewName: "myView",
|
||||
},
|
||||
expected: "bluecatv2: some credentials information are missing: BLUECATV2_SERVER_URL",
|
||||
},
|
||||
{
|
||||
desc: "missing username",
|
||||
envVars: map[string]string{
|
||||
EnvServerURL: "https://example.com/",
|
||||
EnvUsername: "",
|
||||
EnvPassword: "secret",
|
||||
EnvConfigName: "myConfig",
|
||||
EnvViewName: "myView",
|
||||
},
|
||||
expected: "bluecatv2: some credentials information are missing: BLUECATV2_USERNAME",
|
||||
},
|
||||
{
|
||||
desc: "missing password",
|
||||
envVars: map[string]string{
|
||||
EnvServerURL: "https://example.com/",
|
||||
EnvUsername: "userA",
|
||||
EnvPassword: "",
|
||||
EnvConfigName: "myConfig",
|
||||
EnvViewName: "myView",
|
||||
},
|
||||
expected: "bluecatv2: some credentials information are missing: BLUECATV2_PASSWORD",
|
||||
},
|
||||
{
|
||||
desc: "missing configuration name",
|
||||
envVars: map[string]string{
|
||||
EnvServerURL: "https://example.com/",
|
||||
EnvUsername: "userA",
|
||||
EnvPassword: "secret",
|
||||
EnvConfigName: "",
|
||||
EnvViewName: "myView",
|
||||
},
|
||||
expected: "bluecatv2: some credentials information are missing: BLUECATV2_CONFIG_NAME",
|
||||
},
|
||||
{
|
||||
desc: "missing view name",
|
||||
envVars: map[string]string{
|
||||
EnvServerURL: "https://example.com/",
|
||||
EnvUsername: "userA",
|
||||
EnvPassword: "secret",
|
||||
EnvConfigName: "myConfig",
|
||||
EnvViewName: "",
|
||||
},
|
||||
expected: "bluecatv2: some credentials information are missing: BLUECATV2_VIEW_NAME",
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
envVars: map[string]string{},
|
||||
expected: "bluecatv2: some credentials information are missing: BLUECATV2_SERVER_URL,BLUECATV2_USERNAME,BLUECATV2_PASSWORD,BLUECATV2_CONFIG_NAME,BLUECATV2_VIEW_NAME",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
defer envTest.RestoreEnv()
|
||||
|
||||
envTest.ClearEnv()
|
||||
|
||||
envTest.Apply(test.envVars)
|
||||
|
||||
p, err := NewDNSProvider()
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDNSProviderConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
serverURL string
|
||||
username string
|
||||
password string
|
||||
configName string
|
||||
viewName string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
serverURL: "https://example.com/",
|
||||
username: "userA",
|
||||
password: "secret",
|
||||
configName: "myConfig",
|
||||
viewName: "myView",
|
||||
},
|
||||
{
|
||||
desc: "missing server URL",
|
||||
username: "userA",
|
||||
password: "secret",
|
||||
configName: "myConfig",
|
||||
viewName: "myView",
|
||||
expected: "bluecatv2: missing server URL",
|
||||
},
|
||||
{
|
||||
desc: "missing username",
|
||||
serverURL: "https://example.com/",
|
||||
password: "secret",
|
||||
configName: "myConfig",
|
||||
viewName: "myView",
|
||||
expected: "bluecatv2: credentials missing",
|
||||
},
|
||||
{
|
||||
desc: "missing password",
|
||||
serverURL: "https://example.com/",
|
||||
username: "userA",
|
||||
configName: "myConfig",
|
||||
viewName: "myView",
|
||||
expected: "bluecatv2: credentials missing",
|
||||
},
|
||||
{
|
||||
desc: "missing configuration name",
|
||||
serverURL: "https://example.com/",
|
||||
username: "userA",
|
||||
password: "secret",
|
||||
viewName: "myView",
|
||||
expected: "bluecatv2: missing configuration name",
|
||||
},
|
||||
{
|
||||
desc: "missing view name",
|
||||
serverURL: "https://example.com/",
|
||||
username: "userA",
|
||||
password: "secret",
|
||||
configName: "myConfig",
|
||||
expected: "bluecatv2: missing view name",
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
expected: "bluecatv2: missing server URL",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
config := NewDefaultConfig()
|
||||
config.ServerURL = test.serverURL
|
||||
config.Username = test.username
|
||||
config.Password = test.password
|
||||
config.ConfigName = test.configName
|
||||
config.ViewName = test.viewName
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLivePresent(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.Present(t.Context(), envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveCleanUp(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.CleanUp(t.Context(), envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func mockBuilder() *servermock.Builder[*DNSProvider] {
|
||||
return servermock.NewBuilder(
|
||||
func(server *httptest.Server) (*DNSProvider, error) {
|
||||
config := NewDefaultConfig()
|
||||
|
||||
config.ServerURL = server.URL
|
||||
config.Username = "userA"
|
||||
config.Password = "secret"
|
||||
config.ConfigName = "myConfiguration"
|
||||
config.ViewName = "myView"
|
||||
|
||||
config.HTTPClient = server.Client()
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
},
|
||||
servermock.CheckHeader().
|
||||
WithJSONHeaders(),
|
||||
)
|
||||
}
|
||||
|
||||
func TestDNSProvider_Present(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("POST /api/v2/sessions",
|
||||
servermock.ResponseFromInternal("postSession.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("postSession-request.json"),
|
||||
).
|
||||
Route("GET /api/v2/configurations",
|
||||
servermock.ResponseFromInternal("configurations.json"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With("filter", "name:eq('myConfiguration')"),
|
||||
).
|
||||
Route("GET /api/v2/configurations/12345/views",
|
||||
servermock.ResponseFromInternal("views.json"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With("filter", "name:eq('myView')"),
|
||||
).
|
||||
Route("GET /api/v2/zones",
|
||||
http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
filter := req.URL.Query().Get("filter")
|
||||
|
||||
if strings.Contains(filter, internal.Eq("absoluteName", "example.com").String()) {
|
||||
servermock.ResponseFromInternal("zones.json").ServeHTTP(rw, req)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
servermock.ResponseFromInternal("error.json").
|
||||
WithStatusCode(http.StatusNotFound).ServeHTTP(rw, req)
|
||||
}),
|
||||
).
|
||||
Route("POST /api/v2/zones/12345/resourceRecords",
|
||||
servermock.ResponseFromInternal("postZoneResourceRecord.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("postZoneResourceRecord-request.json"),
|
||||
).
|
||||
Route("POST /api/v2/zones/12345/deployments",
|
||||
servermock.ResponseFromInternal("postZoneDeployment.json").
|
||||
WithStatusCode(http.StatusCreated),
|
||||
servermock.CheckRequestJSONBodyFromInternal("postZoneDeployment-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.Present(t.Context(), "example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_Present_skipDeploy(t *testing.T) {
|
||||
defer envTest.RestoreEnv()
|
||||
|
||||
envTest.ClearEnv()
|
||||
|
||||
envTest.Apply(map[string]string{
|
||||
EnvSkipDeploy: "true",
|
||||
})
|
||||
|
||||
provider := mockBuilder().
|
||||
Route("POST /api/v2/sessions",
|
||||
servermock.ResponseFromInternal("postSession.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("postSession-request.json"),
|
||||
).
|
||||
Route("GET /api/v2/configurations",
|
||||
servermock.ResponseFromInternal("configurations.json"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With("filter", "name:eq('myConfiguration')"),
|
||||
).
|
||||
Route("GET /api/v2/configurations/12345/views",
|
||||
servermock.ResponseFromInternal("views.json"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With("filter", "name:eq('myView')"),
|
||||
).
|
||||
Route("GET /api/v2/zones",
|
||||
http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
filter := req.URL.Query().Get("filter")
|
||||
|
||||
if strings.Contains(filter, internal.Eq("absoluteName", "example.com").String()) {
|
||||
servermock.ResponseFromInternal("zones.json").ServeHTTP(rw, req)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
servermock.ResponseFromInternal("error.json").
|
||||
WithStatusCode(http.StatusNotFound).ServeHTTP(rw, req)
|
||||
}),
|
||||
).
|
||||
Route("POST /api/v2/zones/12345/resourceRecords",
|
||||
servermock.ResponseFromInternal("postZoneResourceRecord.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("postZoneResourceRecord-request.json"),
|
||||
).
|
||||
Route("POST /api/v2/zones/456789/deployments",
|
||||
servermock.Noop().
|
||||
WithStatusCode(http.StatusUnauthorized),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.Present(t.Context(), "example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_CleanUp(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("POST /api/v2/sessions",
|
||||
servermock.ResponseFromInternal("postSession.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("postSession-request.json"),
|
||||
).
|
||||
Route("DELETE /api/v2/resourceRecords/12345",
|
||||
servermock.ResponseFromInternal("deleteResourceRecord.json"),
|
||||
).
|
||||
Route("POST /api/v2/zones/456789/deployments",
|
||||
servermock.ResponseFromInternal("postZoneDeployment.json").
|
||||
WithStatusCode(http.StatusCreated),
|
||||
servermock.CheckRequestJSONBodyFromInternal("postZoneDeployment-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
provider.zoneIDs["abc"] = 456789
|
||||
provider.recordIDs["abc"] = 12345
|
||||
|
||||
err := provider.CleanUp(t.Context(), "example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_CleanUp_skipDeploy(t *testing.T) {
|
||||
defer envTest.RestoreEnv()
|
||||
|
||||
envTest.ClearEnv()
|
||||
|
||||
envTest.Apply(map[string]string{
|
||||
EnvSkipDeploy: "true",
|
||||
})
|
||||
|
||||
provider := mockBuilder().
|
||||
Route("POST /api/v2/sessions",
|
||||
servermock.ResponseFromInternal("postSession.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("postSession-request.json"),
|
||||
).
|
||||
Route("DELETE /api/v2/resourceRecords/12345",
|
||||
servermock.ResponseFromInternal("deleteResourceRecord.json"),
|
||||
).
|
||||
Route("POST /api/v2/zones/456789/deployments",
|
||||
servermock.Noop().
|
||||
WithStatusCode(http.StatusUnauthorized),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
provider.zoneIDs["abc"] = 456789
|
||||
provider.recordIDs["abc"] = 12345
|
||||
|
||||
err := provider.CleanUp(t.Context(), "example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
221
providers/dns/bluecatv2/internal/client.go
Normal file
221
providers/dns/bluecatv2/internal/client.go
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/internal/errutils"
|
||||
"github.com/go-acme/lego/v5/internal/useragent"
|
||||
querystring "github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
// Client the Bluecat v2 API client.
|
||||
type Client struct {
|
||||
username string
|
||||
password string
|
||||
|
||||
baseURL *url.URL
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new Client.
|
||||
func NewClient(serverURL, username, password string) (*Client, error) {
|
||||
if serverURL == "" {
|
||||
return nil, errors.New("server URL missing")
|
||||
}
|
||||
|
||||
if username == "" || password == "" {
|
||||
return nil, errors.New("credentials missing")
|
||||
}
|
||||
|
||||
baseURL, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{
|
||||
username: username,
|
||||
password: password,
|
||||
baseURL: baseURL,
|
||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RetrieveZones retrieves all zones.
|
||||
func (c *Client) RetrieveZones(ctx context.Context, opts *CollectionOptions) ([]ZoneResource, error) {
|
||||
endpoint := c.baseURL.JoinPath("api", "v2", "zones")
|
||||
|
||||
collection, err := retrieveCollection[ZoneResource](ctx, c, endpoint, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return collection.Data, nil
|
||||
}
|
||||
|
||||
// RetrieveZoneDeployments retrieves all deployments for a zone.
|
||||
func (c *Client) RetrieveZoneDeployments(ctx context.Context, zoneID int64, opts *CollectionOptions) ([]QuickDeployment, error) {
|
||||
endpoint := c.baseURL.JoinPath("api", "v2", "zones", strconv.FormatInt(zoneID, 10), "deployments")
|
||||
|
||||
collection, err := retrieveCollection[QuickDeployment](ctx, c, endpoint, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return collection.Data, nil
|
||||
}
|
||||
|
||||
// CreateZoneDeployment creates a new deployment for a zone.
|
||||
func (c *Client) CreateZoneDeployment(ctx context.Context, zoneID int64) (*QuickDeployment, error) {
|
||||
endpoint := c.baseURL.JoinPath("api", "v2", "zones", strconv.FormatInt(zoneID, 10), "deployments")
|
||||
|
||||
payload := CommonResource{
|
||||
Type: "QuickDeployment",
|
||||
}
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := new(QuickDeployment)
|
||||
|
||||
err = c.doAuthenticated(ctx, req, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateZoneResourceRecord creates a new TXT record in a zone.
|
||||
func (c *Client) CreateZoneResourceRecord(ctx context.Context, zoneID int64, record RecordTXT) (*RecordTXT, error) {
|
||||
endpoint := c.baseURL.JoinPath("api", "v2", "zones", strconv.FormatInt(zoneID, 10), "resourceRecords")
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := new(RecordTXT)
|
||||
|
||||
err = c.doAuthenticated(ctx, req, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeleteResourceRecord deletes a resource record.
|
||||
func (c *Client) DeleteResourceRecord(ctx context.Context, recordID int64) error {
|
||||
endpoint := c.baseURL.JoinPath("api", "v2", "resourceRecords", strconv.FormatInt(recordID, 10))
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.doAuthenticated(ctx, req, nil)
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request, result any) error {
|
||||
useragent.SetHeader(req.Header)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return errutils.NewHTTPDoError(req, err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
return parseError(req, resp)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errutils.NewReadResponseError(req, resp.StatusCode, err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(raw, result)
|
||||
if err != nil {
|
||||
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func retrieveCollection[T any](ctx context.Context, client *Client, endpoint *url.URL, opts *CollectionOptions) (*Collection[T], error) {
|
||||
if opts != nil {
|
||||
values, err := querystring.Values(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint.RawQuery = values.Encode()
|
||||
}
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &Collection[T]{}
|
||||
|
||||
err = client.doAuthenticated(ctx, req, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if payload != nil {
|
||||
err := json.NewEncoder(buf).Encode(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request JSON body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
if payload != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func parseError(req *http.Request, resp *http.Response) error {
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var errAPI APIError
|
||||
|
||||
err := json.Unmarshal(raw, &errAPI)
|
||||
if err != nil {
|
||||
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
|
||||
}
|
||||
|
||||
return &errAPI
|
||||
}
|
||||
208
providers/dns/bluecatv2/internal/client_test.go
Normal file
208
providers/dns/bluecatv2/internal/client_test.go
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func mockBuilderAuthenticated() *servermock.Builder[*Client] {
|
||||
return servermock.NewBuilder[*Client](
|
||||
func(server *httptest.Server) (*Client, error) {
|
||||
client, err := NewClient(server.URL, "userA", "secret")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.baseURL, _ = url.Parse(server.URL)
|
||||
client.HTTPClient = server.Client()
|
||||
|
||||
return client, nil
|
||||
},
|
||||
servermock.CheckHeader().
|
||||
WithJSONHeaders(),
|
||||
servermock.CheckHeader().
|
||||
WithAuthorization("Basic secretToken"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestClient_RetrieveZones(t *testing.T) {
|
||||
client := mockBuilderAuthenticated().
|
||||
Route("GET /api/v2/zones",
|
||||
servermock.ResponseFromFixture("zones.json"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With(
|
||||
"filter",
|
||||
"absoluteName:eq('example.com') and configuration.name:eq('myConfiguration') and view.name:eq('myView')",
|
||||
),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
opts := &CollectionOptions{
|
||||
Filter: And(
|
||||
Eq("absoluteName", "example.com"),
|
||||
Eq("configuration.name", "myConfiguration"),
|
||||
Eq("view.name", "myView"),
|
||||
).String(),
|
||||
}
|
||||
|
||||
result, err := client.RetrieveZones(mockToken(t.Context()), opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := []ZoneResource{
|
||||
{
|
||||
CommonResource: CommonResource{ID: 12345, Type: "ENUMZone", Name: "5678"},
|
||||
AbsoluteName: "string",
|
||||
},
|
||||
{
|
||||
CommonResource: CommonResource{ID: 12345, Type: "ExternalHostsZone", Name: "name"},
|
||||
},
|
||||
{
|
||||
CommonResource: CommonResource{ID: 12345, Type: "InternalRootZone", Name: "name"},
|
||||
},
|
||||
{
|
||||
CommonResource: CommonResource{ID: 12345, Type: "ResponsePolicyZone", Name: "name"},
|
||||
},
|
||||
{
|
||||
CommonResource: CommonResource{ID: 12345, Type: "Zone", Name: "example.com"},
|
||||
AbsoluteName: "example.com",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestClient_RetrieveZones_error(t *testing.T) {
|
||||
client := mockBuilderAuthenticated().
|
||||
Route("GET /api/v2/zones",
|
||||
servermock.ResponseFromFixture("error.json").
|
||||
WithStatusCode(http.StatusUnauthorized),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
opts := &CollectionOptions{
|
||||
Filter: And(
|
||||
Eq("absoluteName", "example.com"),
|
||||
Eq("configuration.name", "myConfiguration"),
|
||||
Eq("view.name", "myView"),
|
||||
).String(),
|
||||
}
|
||||
|
||||
_, err := client.RetrieveZones(mockToken(t.Context()), opts)
|
||||
require.EqualError(t, err, "401: Unauthorized: InvalidAuthorizationToken: The provided authorization token is invalid")
|
||||
}
|
||||
|
||||
func TestClient_RetrieveZoneDeployments(t *testing.T) {
|
||||
client := mockBuilderAuthenticated().
|
||||
Route("GET /api/v2/zones/456789/deployments",
|
||||
servermock.ResponseFromFixture("getZoneDeployments.json"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With("filter", "id:eq('12345')"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
opts := &CollectionOptions{
|
||||
Filter: Eq("id", "12345").String(),
|
||||
}
|
||||
|
||||
result, err := client.RetrieveZoneDeployments(mockToken(t.Context()), 456789, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := []QuickDeployment{
|
||||
{
|
||||
CommonResource: CommonResource{ID: 12345, Type: "QuickDeployment", Name: ""},
|
||||
State: "PENDING",
|
||||
Status: "CANCEL",
|
||||
Message: "string",
|
||||
PercentComplete: 50,
|
||||
CreationDateTime: time.Date(2022, time.November, 23, 2, 53, 0, 0, time.UTC),
|
||||
StartDateTime: time.Date(2022, time.November, 23, 2, 53, 3, 0, time.UTC),
|
||||
CompletionDateTime: time.Date(2022, time.November, 23, 2, 54, 5, 0, time.UTC),
|
||||
Method: "SCHEDULED",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestClient_CreateZoneDeployment(t *testing.T) {
|
||||
client := mockBuilderAuthenticated().
|
||||
Route("POST /api/v2/zones/12345/deployments",
|
||||
servermock.ResponseFromFixture("postZoneDeployment.json").
|
||||
WithStatusCode(http.StatusCreated),
|
||||
servermock.CheckRequestJSONBodyFromFixture("postZoneDeployment-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
quickDeployment, err := client.CreateZoneDeployment(mockToken(t.Context()), 12345)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &QuickDeployment{
|
||||
CommonResource: CommonResource{ID: 12345, Type: "QuickDeployment"},
|
||||
State: "PENDING",
|
||||
Status: "CANCEL",
|
||||
Message: "string",
|
||||
PercentComplete: 50,
|
||||
CreationDateTime: time.Date(2022, time.November, 23, 2, 53, 0, 0, time.UTC),
|
||||
StartDateTime: time.Date(2022, time.November, 23, 2, 53, 3, 0, time.UTC),
|
||||
CompletionDateTime: time.Date(2022, time.November, 23, 2, 54, 5, 0, time.UTC),
|
||||
Method: "SCHEDULED",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, quickDeployment)
|
||||
}
|
||||
|
||||
func TestClient_CreateZoneResourceRecord(t *testing.T) {
|
||||
client := mockBuilderAuthenticated().
|
||||
Route("POST /api/v2/zones/12345/resourceRecords",
|
||||
servermock.ResponseFromFixture("postZoneResourceRecord.json"),
|
||||
servermock.CheckRequestJSONBodyFromFixture("postZoneResourceRecord-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
record := RecordTXT{
|
||||
CommonResource: CommonResource{
|
||||
Type: "TXTRecord",
|
||||
Name: "_acme-challenge",
|
||||
},
|
||||
Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
TTL: 120,
|
||||
RecordType: "TXT",
|
||||
}
|
||||
|
||||
result, err := client.CreateZoneResourceRecord(mockToken(t.Context()), 12345, record)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &RecordTXT{
|
||||
CommonResource: CommonResource{
|
||||
ID: 12345,
|
||||
Type: "ResourceRecord",
|
||||
Name: "name",
|
||||
},
|
||||
TTL: 3600,
|
||||
AbsoluteName: "host1.example.com",
|
||||
Comment: "Sample comment.",
|
||||
Dynamic: true,
|
||||
RecordType: "CNAME",
|
||||
Text: "",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestClient_DeleteResourceRecord(t *testing.T) {
|
||||
client := mockBuilderAuthenticated().
|
||||
Route("DELETE /api/v2/resourceRecords/12345",
|
||||
servermock.ResponseFromFixture("deleteResourceRecord.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := client.DeleteResourceRecord(mockToken(t.Context()), 12345)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"id": 12345,
|
||||
"type": "WorkflowRequest",
|
||||
"state": "APPROVED",
|
||||
"operation": "ADD_ALIAS_RECORD",
|
||||
"creator": {
|
||||
"id": 103307,
|
||||
"type": "User",
|
||||
"name": "admin",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"authenticator": {
|
||||
"id": 12345,
|
||||
"type": "Authenticator",
|
||||
"name": "LDAP authenticator"
|
||||
},
|
||||
"email": "user@example.com",
|
||||
"phoneNumber": "555-1234",
|
||||
"securityPrivilege": "NO_ACCESS",
|
||||
"historyPrivilege": "HIDE",
|
||||
"accessType": "GUI",
|
||||
"passwordResetRequired": true,
|
||||
"accountLocked": true,
|
||||
"x509Required": true,
|
||||
"administrativeAccessRights": [
|
||||
{
|
||||
"resourceType": "Event",
|
||||
"accessLevel": "HIDE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"resourceId": 0,
|
||||
"resourceType": "ACL",
|
||||
"fieldUpdates": [
|
||||
{
|
||||
"name": "string",
|
||||
"value": {},
|
||||
"previousValue": {}
|
||||
}
|
||||
],
|
||||
"dependentRequest": "string",
|
||||
"modifier": {
|
||||
"id": 103307,
|
||||
"type": "User",
|
||||
"name": "admin",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"authenticator": {
|
||||
"id": 12345,
|
||||
"type": "Authenticator",
|
||||
"name": "LDAP authenticator"
|
||||
},
|
||||
"email": "user@example.com",
|
||||
"phoneNumber": "555-1234",
|
||||
"securityPrivilege": "NO_ACCESS",
|
||||
"historyPrivilege": "HIDE",
|
||||
"accessType": "GUI",
|
||||
"passwordResetRequired": true,
|
||||
"accountLocked": true,
|
||||
"x509Required": true,
|
||||
"administrativeAccessRights": [
|
||||
{
|
||||
"resourceType": "Event",
|
||||
"accessLevel": "HIDE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"creationDateTime": "2022-10-17T19:11:45Z",
|
||||
"modificationDateTime": "2022-10-18T19:11:45Z",
|
||||
"comment": "Sample comment."
|
||||
}
|
||||
6
providers/dns/bluecatv2/internal/fixtures/error.json
Normal file
6
providers/dns/bluecatv2/internal/fixtures/error.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"status": 401,
|
||||
"reason": "Unauthorized",
|
||||
"code": "InvalidAuthorizationToken",
|
||||
"message": "The provided authorization token is invalid"
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"count": 0,
|
||||
"totalCount": 0,
|
||||
"data": [
|
||||
{
|
||||
"id": 12345,
|
||||
"type": "QuickDeployment",
|
||||
"state": "PENDING",
|
||||
"status": "CANCEL",
|
||||
"message": "string",
|
||||
"percentComplete": 50,
|
||||
"creationDateTime": "2022-11-23T02:53:00Z",
|
||||
"startDateTime": "2022-11-23T02:53:03Z",
|
||||
"completionDateTime": "2022-11-23T02:54:05Z",
|
||||
"user": {
|
||||
"id": 103307,
|
||||
"type": "User",
|
||||
"name": "admin",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"authenticator": {
|
||||
"id": 12345,
|
||||
"type": "Authenticator",
|
||||
"name": "LDAP authenticator"
|
||||
},
|
||||
"email": "user@example.com",
|
||||
"phoneNumber": "555-1234",
|
||||
"securityPrivilege": "NO_ACCESS",
|
||||
"historyPrivilege": "HIDE",
|
||||
"accessType": "GUI",
|
||||
"passwordResetRequired": true,
|
||||
"accountLocked": true,
|
||||
"x509Required": true,
|
||||
"administrativeAccessRights": [
|
||||
{
|
||||
"resourceType": "Event",
|
||||
"accessLevel": "HIDE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"method": "SCHEDULED"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"username": "userA",
|
||||
"password": "secret"
|
||||
}
|
||||
50
providers/dns/bluecatv2/internal/fixtures/postSession.json
Normal file
50
providers/dns/bluecatv2/internal/fixtures/postSession.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"id": 12345,
|
||||
"type": "UserSession",
|
||||
"apiToken": "VZoO2Z0BjBaJyvuhE4vNJRWqI9upwDHk70UNi0Ez",
|
||||
"apiTokenExpirationDateTime": "2022-09-15T17:52:07Z",
|
||||
"basicAuthenticationCredentials": "YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=",
|
||||
"remoteAddress": "192.168.1.1",
|
||||
"readOnly": true,
|
||||
"loginDateTime": "2022-09-14T17:45:03Z",
|
||||
"logoutDateTime": "2022-09-14T19:45:03Z",
|
||||
"state": "LOGGED_IN",
|
||||
"response": "Authentication Error: Ensure that your username and password are correct.",
|
||||
"user": {
|
||||
"id": 103307,
|
||||
"type": "User",
|
||||
"name": "admin",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"authenticator": {
|
||||
"id": 12345,
|
||||
"type": "Authenticator",
|
||||
"name": "LDAP authenticator"
|
||||
},
|
||||
"email": "user@example.com",
|
||||
"phoneNumber": "555-1234",
|
||||
"securityPrivilege": "NO_ACCESS",
|
||||
"historyPrivilege": "HIDE",
|
||||
"accessType": "GUI",
|
||||
"passwordResetRequired": true,
|
||||
"accountLocked": true,
|
||||
"x509Required": true,
|
||||
"administrativeAccessRights": [
|
||||
{
|
||||
"resourceType": "Event",
|
||||
"accessLevel": "HIDE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"authenticator": {
|
||||
"id": 12345,
|
||||
"type": "Authenticator",
|
||||
"name": "LDAP authenticator",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"type": "QuickDeployment"
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"id": 12345,
|
||||
"type": "QuickDeployment",
|
||||
"state": "PENDING",
|
||||
"status": "CANCEL",
|
||||
"message": "string",
|
||||
"percentComplete": 50,
|
||||
"creationDateTime": "2022-11-23T02:53:00Z",
|
||||
"startDateTime": "2022-11-23T02:53:03Z",
|
||||
"completionDateTime": "2022-11-23T02:54:05Z",
|
||||
"user": {
|
||||
"id": 103307,
|
||||
"type": "User",
|
||||
"name": "admin",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"authenticator": {
|
||||
"id": 12345,
|
||||
"type": "Authenticator",
|
||||
"name": "LDAP authenticator"
|
||||
},
|
||||
"email": "user@example.com",
|
||||
"phoneNumber": "555-1234",
|
||||
"securityPrivilege": "NO_ACCESS",
|
||||
"historyPrivilege": "HIDE",
|
||||
"accessType": "GUI",
|
||||
"passwordResetRequired": true,
|
||||
"accountLocked": true,
|
||||
"x509Required": true,
|
||||
"administrativeAccessRights": [
|
||||
{
|
||||
"resourceType": "Event",
|
||||
"accessLevel": "HIDE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"method": "SCHEDULED"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "TXTRecord",
|
||||
"name": "_acme-challenge",
|
||||
"ttl": 120,
|
||||
"recordType": "TXT",
|
||||
"text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"id": 12345,
|
||||
"type": "ResourceRecord",
|
||||
"name": "name",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"configuration": {
|
||||
"id": 12345,
|
||||
"type": "Configuration",
|
||||
"name": "name"
|
||||
},
|
||||
"ttl": 3600,
|
||||
"absoluteName": "host1.example.com",
|
||||
"comment": "Sample comment.",
|
||||
"dynamic": true,
|
||||
"recordType": "CNAME",
|
||||
"linkedRecord": {
|
||||
"id": 12345,
|
||||
"type": "ResourceRecord",
|
||||
"name": "name",
|
||||
"absoluteName": "host1.example.com"
|
||||
}
|
||||
}
|
||||
185
providers/dns/bluecatv2/internal/fixtures/zones.json
Normal file
185
providers/dns/bluecatv2/internal/fixtures/zones.json
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
{
|
||||
"count": 0,
|
||||
"totalCount": 0,
|
||||
"data": [
|
||||
{
|
||||
"id": 12345,
|
||||
"type": "ENUMZone",
|
||||
"name": "5678",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"configuration": {
|
||||
"id": 12345,
|
||||
"type": "Configuration",
|
||||
"name": "name"
|
||||
},
|
||||
"view": {
|
||||
"id": 12345,
|
||||
"type": "View",
|
||||
"name": "default",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"configuration": {
|
||||
"id": 12345,
|
||||
"type": "Configuration",
|
||||
"name": "name"
|
||||
},
|
||||
"deviceRegistrationEnabled": true,
|
||||
"deviceRegistrationPortalAddress": "10.10.10.10"
|
||||
},
|
||||
"deploymentEnabled": true,
|
||||
"absoluteName": "string"
|
||||
},
|
||||
{
|
||||
"id": 12345,
|
||||
"type": "ExternalHostsZone",
|
||||
"name": "name",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"configuration": {
|
||||
"id": 12345,
|
||||
"type": "Configuration",
|
||||
"name": "name"
|
||||
},
|
||||
"view": {
|
||||
"id": 12345,
|
||||
"type": "View",
|
||||
"name": "default",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"configuration": {
|
||||
"id": 12345,
|
||||
"type": "Configuration",
|
||||
"name": "name"
|
||||
},
|
||||
"deviceRegistrationEnabled": true,
|
||||
"deviceRegistrationPortalAddress": "10.10.10.10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 12345,
|
||||
"type": "InternalRootZone",
|
||||
"name": "name",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"configuration": {
|
||||
"id": 12345,
|
||||
"type": "Configuration",
|
||||
"name": "name"
|
||||
},
|
||||
"view": {
|
||||
"id": 12345,
|
||||
"type": "View",
|
||||
"name": "default",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"configuration": {
|
||||
"id": 12345,
|
||||
"type": "Configuration",
|
||||
"name": "name"
|
||||
},
|
||||
"deviceRegistrationEnabled": true,
|
||||
"deviceRegistrationPortalAddress": "10.10.10.10"
|
||||
},
|
||||
"deploymentEnabled": true
|
||||
},
|
||||
{
|
||||
"id": 12345,
|
||||
"type": "ResponsePolicyZone",
|
||||
"name": "name",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"configuration": {
|
||||
"id": 12345,
|
||||
"type": "Configuration",
|
||||
"name": "name"
|
||||
},
|
||||
"view": {
|
||||
"id": 12345,
|
||||
"type": "View",
|
||||
"name": "default",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"configuration": {
|
||||
"id": 12345,
|
||||
"type": "Configuration",
|
||||
"name": "name"
|
||||
},
|
||||
"deviceRegistrationEnabled": true,
|
||||
"deviceRegistrationPortalAddress": "10.10.10.10"
|
||||
},
|
||||
"responsePolicyZoneType": "LOCAL",
|
||||
"responsePolicy": {
|
||||
"id": 12345,
|
||||
"type": "ResponsePolicy",
|
||||
"name": "Block Response Policy"
|
||||
},
|
||||
"overridePolicyType": "ALLOWLIST",
|
||||
"overrideRefreshTime": "string",
|
||||
"redirectTarget": "string",
|
||||
"feedCategories": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12345,
|
||||
"type": "Zone",
|
||||
"name": "example.com",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"configuration": {
|
||||
"id": 12345,
|
||||
"type": "Configuration",
|
||||
"name": "name"
|
||||
},
|
||||
"view": {
|
||||
"id": 12345,
|
||||
"type": "View",
|
||||
"name": "default",
|
||||
"userDefinedFields": {
|
||||
"udf1": "value1",
|
||||
"udf2": "value2"
|
||||
},
|
||||
"configuration": {
|
||||
"id": 12345,
|
||||
"type": "Configuration",
|
||||
"name": "name"
|
||||
},
|
||||
"deviceRegistrationEnabled": true,
|
||||
"deviceRegistrationPortalAddress": "10.10.10.10"
|
||||
},
|
||||
"deploymentEnabled": true,
|
||||
"dynamicUpdateEnabled": true,
|
||||
"template": {
|
||||
"id": 12345,
|
||||
"type": "ZoneTemplate",
|
||||
"name": "name"
|
||||
},
|
||||
"signed": true,
|
||||
"signingPolicy": {
|
||||
"id": 12345,
|
||||
"type": "DNSSECSigningPolicy",
|
||||
"name": "name"
|
||||
},
|
||||
"absoluteName": "example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
60
providers/dns/bluecatv2/internal/identity.go
Normal file
60
providers/dns/bluecatv2/internal/identity.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type token string
|
||||
|
||||
const tokenKey token = "token"
|
||||
|
||||
const authorizationHeader = "Authorization"
|
||||
|
||||
// CreateSession creates a new session.
|
||||
func (c *Client) CreateSession(ctx context.Context, info LoginInfo) (*Session, error) {
|
||||
endpoint := c.baseURL.JoinPath("api", "v2", "sessions")
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := new(Session)
|
||||
|
||||
err = c.do(req, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateAuthenticatedContext creates a new authenticated context.
|
||||
func (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) {
|
||||
tok, err := c.CreateSession(ctx, LoginInfo{Username: c.username, Password: c.password})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create session: %w", err)
|
||||
}
|
||||
|
||||
return context.WithValue(ctx, tokenKey, tok.BasicAuthenticationCredentials), nil
|
||||
}
|
||||
|
||||
func (c *Client) doAuthenticated(ctx context.Context, req *http.Request, result any) error {
|
||||
tok := getToken(ctx)
|
||||
if tok != "" {
|
||||
req.Header.Set(authorizationHeader, "Basic "+tok)
|
||||
}
|
||||
|
||||
return c.do(req, result)
|
||||
}
|
||||
|
||||
func getToken(ctx context.Context) string {
|
||||
tok, ok := ctx.Value(tokenKey).(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return tok
|
||||
}
|
||||
82
providers/dns/bluecatv2/internal/identity_test.go
Normal file
82
providers/dns/bluecatv2/internal/identity_test.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func mockBuilder() *servermock.Builder[*Client] {
|
||||
return servermock.NewBuilder[*Client](
|
||||
func(server *httptest.Server) (*Client, error) {
|
||||
client, err := NewClient(server.URL, "userA", "secret")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.baseURL, _ = url.Parse(server.URL)
|
||||
client.HTTPClient = server.Client()
|
||||
|
||||
return client, nil
|
||||
},
|
||||
servermock.CheckHeader().
|
||||
WithJSONHeaders(),
|
||||
)
|
||||
}
|
||||
|
||||
func mockToken(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, tokenKey, "secretToken")
|
||||
}
|
||||
|
||||
func TestClient_CreateSession(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("POST /api/v2/sessions",
|
||||
servermock.ResponseFromFixture("postSession.json"),
|
||||
servermock.CheckRequestJSONBodyFromFixture("postSession-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
info := LoginInfo{
|
||||
Username: "userA",
|
||||
Password: "secret",
|
||||
}
|
||||
|
||||
result, err := client.CreateSession(mockToken(t.Context()), info)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Session{
|
||||
ID: 12345,
|
||||
Type: "UserSession",
|
||||
APIToken: "VZoO2Z0BjBaJyvuhE4vNJRWqI9upwDHk70UNi0Ez",
|
||||
APITokenExpirationDateTime: time.Date(2022, time.September, 15, 17, 52, 7, 0, time.UTC),
|
||||
BasicAuthenticationCredentials: "YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=",
|
||||
RemoteAddress: "192.168.1.1",
|
||||
ReadOnly: true,
|
||||
LoginDateTime: time.Date(2022, time.September, 14, 17, 45, 3, 0, time.UTC),
|
||||
LogoutDateTime: time.Date(2022, time.September, 14, 19, 45, 3, 0, time.UTC),
|
||||
State: "LOGGED_IN",
|
||||
Response: "Authentication Error: Ensure that your username and password are correct.",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestClient_CreateAuthenticatedContext(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("POST /api/v2/sessions",
|
||||
servermock.ResponseFromFixture("postSession.json"),
|
||||
servermock.CheckRequestJSONBodyFromFixture("postSession-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
ctx, err := client.CreateAuthenticatedContext(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=", getToken(ctx))
|
||||
}
|
||||
64
providers/dns/bluecatv2/internal/predicates.go
Normal file
64
providers/dns/bluecatv2/internal/predicates.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Predicate struct {
|
||||
field string
|
||||
operator string
|
||||
values []string
|
||||
}
|
||||
|
||||
func (p *Predicate) String() string {
|
||||
var values []string
|
||||
for _, v := range p.values {
|
||||
values = append(values, fmt.Sprintf("'%s'", v))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%s(%s)", p.field, p.operator, strings.Join(values, ", "))
|
||||
}
|
||||
|
||||
func Eq(field, value string) *Predicate {
|
||||
return &Predicate{field: field, operator: "eq", values: []string{value}}
|
||||
}
|
||||
|
||||
func Contains(field, value string) *Predicate {
|
||||
return &Predicate{field: field, operator: "contains", values: []string{value}}
|
||||
}
|
||||
|
||||
func StartsWith(field, value string) *Predicate {
|
||||
return &Predicate{field: field, operator: "startsWith", values: []string{value}}
|
||||
}
|
||||
|
||||
func EndsWith(field, value string) *Predicate {
|
||||
return &Predicate{field: field, operator: "endsWith", values: []string{value}}
|
||||
}
|
||||
|
||||
func In(field string, values ...string) *Predicate {
|
||||
return &Predicate{field: field, operator: "in", values: values}
|
||||
}
|
||||
|
||||
type Combined struct {
|
||||
predicates []*Predicate
|
||||
operator string
|
||||
}
|
||||
|
||||
func (o *Combined) String() string {
|
||||
var parts []string
|
||||
|
||||
for _, predicate := range o.predicates {
|
||||
parts = append(parts, predicate.String())
|
||||
}
|
||||
|
||||
return strings.Join(parts, " "+o.operator+" ")
|
||||
}
|
||||
|
||||
func And(predicates ...*Predicate) *Combined {
|
||||
return &Combined{predicates: predicates, operator: "and"}
|
||||
}
|
||||
|
||||
func Or(predicates ...*Predicate) *Combined {
|
||||
return &Combined{predicates: predicates, operator: "or"}
|
||||
}
|
||||
78
providers/dns/bluecatv2/internal/predicates_test.go
Normal file
78
providers/dns/bluecatv2/internal/predicates_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPredicate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
predicate fmt.Stringer
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "Equals",
|
||||
predicate: Eq("foo", "bar"),
|
||||
expected: "foo:eq('bar')",
|
||||
},
|
||||
{
|
||||
desc: "Contains",
|
||||
predicate: Contains("foo", "bar"),
|
||||
expected: "foo:contains('bar')",
|
||||
},
|
||||
{
|
||||
desc: "Starts with",
|
||||
predicate: StartsWith("foo", "bar"),
|
||||
expected: "foo:startsWith('bar')",
|
||||
},
|
||||
{
|
||||
desc: "Ends with",
|
||||
predicate: EndsWith("foo", "bar"),
|
||||
expected: "foo:endsWith('bar')",
|
||||
},
|
||||
{
|
||||
desc: "Match a list of values",
|
||||
predicate: In("foo", "bar", "bir"),
|
||||
expected: "foo:in('bar', 'bir')",
|
||||
},
|
||||
{
|
||||
desc: "Combined: and",
|
||||
predicate: And(Eq("foo", "bar"), Eq("fii", "bir")),
|
||||
expected: "foo:eq('bar') and fii:eq('bir')",
|
||||
},
|
||||
{
|
||||
desc: "Combined: multiple and",
|
||||
predicate: And(
|
||||
Eq("foo", "bar"),
|
||||
Eq("fii", "bir"),
|
||||
Eq("fuu", "bur"),
|
||||
),
|
||||
expected: "foo:eq('bar') and fii:eq('bir') and fuu:eq('bur')",
|
||||
},
|
||||
{
|
||||
desc: "Combined: or",
|
||||
predicate: Or(Eq("foo", "bar"), Eq("foo", "bir")),
|
||||
expected: "foo:eq('bar') or foo:eq('bir')",
|
||||
},
|
||||
{
|
||||
desc: "Combined: multiple or",
|
||||
predicate: Or(
|
||||
Eq("foo", "bar"),
|
||||
Eq("foo", "bir"),
|
||||
Eq("foo", "bur"),
|
||||
),
|
||||
expected: "foo:eq('bar') or foo:eq('bir') or foo:eq('bur')",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, test.expected, test.predicate.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
122
providers/dns/bluecatv2/internal/types.go
Normal file
122
providers/dns/bluecatv2/internal/types.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Quick deployment states.
|
||||
//
|
||||
//nolint:misspell // US vs UK
|
||||
const (
|
||||
QDStatePending = "PENDING"
|
||||
QDStateQueued = "QUEUED"
|
||||
QDStateRunning = "RUNNING"
|
||||
QDStateCancelled = "CANCELLED"
|
||||
QDStateCancelling = "CANCELLING"
|
||||
QDStateCompleted = "COMPLETED"
|
||||
QDStateCompletedWithErrors = "COMPLETED_WITH_ERRORS"
|
||||
QDStateCompletedWithWarnings = "COMPLETED_WITH_WARNINGS"
|
||||
QDStateFailed = "FAILED"
|
||||
QDStateUnknown = "UNKNOWN"
|
||||
)
|
||||
|
||||
// APIError represents an error.
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Errors/9.6.0
|
||||
type APIError struct {
|
||||
Status int `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (a *APIError) Error() string {
|
||||
return fmt.Sprintf("%d: %s: %s: %s", a.Status, a.Reason, a.Code, a.Message)
|
||||
}
|
||||
|
||||
// CommonResource represents the common resource fields.
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Resources/9.6.0
|
||||
type CommonResource struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// Collection represents a collection of resources.
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Collections/9.6.0
|
||||
type Collection[T any] struct {
|
||||
Count int64 `json:"count"`
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
Data []T `json:"data"`
|
||||
}
|
||||
|
||||
type CollectionOptions struct {
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Fields/9.6.0
|
||||
Fields string `url:"fields,omitempty"`
|
||||
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Pagination/9.6.0
|
||||
Limit int `url:"limit,omitempty"`
|
||||
Offset int `url:"offset,omitempty"`
|
||||
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Filter/9.6.0
|
||||
Filter string `url:"filter,omitempty"`
|
||||
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Ordering/9.6.0
|
||||
OrderBy string `url:"orderBy,omitempty"`
|
||||
|
||||
// Should return or not the total number of resources matching the query.
|
||||
Total bool `url:"total,omitempty"`
|
||||
}
|
||||
|
||||
type RecordTXT struct {
|
||||
CommonResource
|
||||
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
AbsoluteName string `json:"absoluteName,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Dynamic bool `json:"dynamic,omitempty"`
|
||||
RecordType string `json:"recordType,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type ZoneResource struct {
|
||||
CommonResource
|
||||
|
||||
AbsoluteName string `json:"absoluteName,omitempty"`
|
||||
}
|
||||
|
||||
type QuickDeployment struct {
|
||||
CommonResource
|
||||
|
||||
State string `json:"state,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
PercentComplete int `json:"percentComplete,omitempty"`
|
||||
CreationDateTime time.Time `json:"creationDateTime,omitzero"`
|
||||
StartDateTime time.Time `json:"startDateTime,omitzero"`
|
||||
CompletionDateTime time.Time `json:"completionDateTime,omitzero"`
|
||||
Method string `json:"method,omitempty"`
|
||||
}
|
||||
|
||||
// LoginInfo represents the login information.
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Creating-an-API-session/9.6.0
|
||||
type LoginInfo struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// Session represents the session.
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Creating-an-API-session/9.6.0
|
||||
type Session struct {
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
APIToken string `json:"apiToken"`
|
||||
APITokenExpirationDateTime time.Time `json:"apiTokenExpirationDateTime"`
|
||||
BasicAuthenticationCredentials string `json:"basicAuthenticationCredentials"`
|
||||
RemoteAddress string `json:"remoteAddress"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
LoginDateTime time.Time `json:"loginDateTime"`
|
||||
LogoutDateTime time.Time `json:"logoutDateTime"`
|
||||
State string `json:"state"`
|
||||
Response string `json:"response"`
|
||||
}
|
||||
|
|
@ -42,13 +42,13 @@ type ErrorChain struct {
|
|||
type Errors []Message
|
||||
|
||||
func (e Errors) Error() string {
|
||||
var msg strings.Builder
|
||||
msg := new(strings.Builder)
|
||||
|
||||
for _, item := range e {
|
||||
msg.WriteString(fmt.Sprintf("%d: %s", item.Code, item.Message))
|
||||
_, _ = fmt.Fprintf(msg, "%d: %s", item.Code, item.Message)
|
||||
|
||||
for _, link := range item.ErrorChain {
|
||||
msg.WriteString(fmt.Sprintf("; %d: %s", link.Code, link.Message))
|
||||
_, _ = fmt.Fprintf(msg, "; %d: %s", link.Code, link.Message)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,12 +29,12 @@ type APIResponse struct {
|
|||
}
|
||||
|
||||
func (a APIResponse) Error() string {
|
||||
var msg strings.Builder
|
||||
msg := new(strings.Builder)
|
||||
|
||||
msg.WriteString(fmt.Sprintf("%s (code=%d)", a.Message, a.Code))
|
||||
_, _ = fmt.Fprintf(msg, "%s (code=%d)", a.Message, a.Code)
|
||||
|
||||
for _, detail := range a.Details {
|
||||
msg.WriteString(fmt.Sprintf(", %s", detail))
|
||||
_, _ = fmt.Fprintf(msg, ", %s", detail)
|
||||
}
|
||||
|
||||
return msg.String()
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ type APIError struct {
|
|||
}
|
||||
|
||||
func (a APIError) Error() string {
|
||||
var msg strings.Builder
|
||||
msg := new(strings.Builder)
|
||||
|
||||
msg.WriteString(fmt.Sprintf("%s: %s", a.Code, a.Message))
|
||||
_, _ = fmt.Fprintf(msg, "%s: %s", a.Code, a.Message)
|
||||
|
||||
for _, field := range a.Fields {
|
||||
msg.WriteString(" ")
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ type APIError struct {
|
|||
}
|
||||
|
||||
func (a *APIError) Error() string {
|
||||
var msg strings.Builder
|
||||
msg := new(strings.Builder)
|
||||
|
||||
msg.WriteString(fmt.Sprintf("status: %s, error: %s", a.Status, a.ErrorMsg))
|
||||
_, _ = fmt.Fprintf(msg, "status: %s, error: %s", a.Status, a.ErrorMsg)
|
||||
|
||||
if a.Code != 0 {
|
||||
msg.WriteString(fmt.Sprintf(", code: %d", a.Code))
|
||||
_, _ = fmt.Fprintf(msg, ", code: %d", a.Code)
|
||||
}
|
||||
|
||||
if len(a.Context) != 0 {
|
||||
for k, v := range a.Context {
|
||||
msg.WriteString(fmt.Sprintf(", %s: %s", k, v))
|
||||
_, _ = fmt.Fprintf(msg, ", %s: %s", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,20 +16,20 @@ type ErrorInfo struct {
|
|||
}
|
||||
|
||||
func (i *ErrorInfo) Error() string {
|
||||
var msg strings.Builder
|
||||
msg := new(strings.Builder)
|
||||
|
||||
msg.WriteString(fmt.Sprintf("%s: %s", i.Code, i.Message))
|
||||
_, _ = fmt.Fprintf(msg, "%s: %s", i.Code, i.Message)
|
||||
|
||||
if i.Details.Announcement != "" {
|
||||
msg.WriteString(fmt.Sprintf(": %s", i.Details.Announcement))
|
||||
_, _ = fmt.Fprintf(msg, ": %s", i.Details.Announcement)
|
||||
}
|
||||
|
||||
for _, limit := range i.Details.Limits {
|
||||
msg.WriteString(fmt.Sprintf("limit: %s", limit.Name))
|
||||
_, _ = fmt.Fprintf(msg, "limit: %s", limit.Name)
|
||||
}
|
||||
|
||||
for _, field := range i.Details.Fields {
|
||||
msg.WriteString(fmt.Sprintf("field: %s: %s", field.Name, strings.Join(field.Messages, ", ")))
|
||||
_, _ = fmt.Fprintf(msg, "field: %s: %s", field.Name, strings.Join(field.Messages, ", "))
|
||||
}
|
||||
|
||||
return msg.String()
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ type APIError struct {
|
|||
}
|
||||
|
||||
func (a *APIError) Error() string {
|
||||
var msg strings.Builder
|
||||
msg := new(strings.Builder)
|
||||
|
||||
msg.WriteString(fmt.Sprintf("%s: %s", a.CorrelationID, a.Message))
|
||||
_, _ = fmt.Fprintf(msg, "%s: %s", a.CorrelationID, a.Message)
|
||||
|
||||
for field, values := range a.Errors {
|
||||
msg.WriteString(fmt.Sprintf(": %s: %s", field, strings.Join(values, ", ")))
|
||||
_, _ = fmt.Fprintf(msg, ": %s: %s", field, strings.Join(values, ", "))
|
||||
}
|
||||
|
||||
return msg.String()
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ type APIError struct {
|
|||
}
|
||||
|
||||
func (a APIError) Error() string {
|
||||
var msg strings.Builder
|
||||
msg := new(strings.Builder)
|
||||
|
||||
msg.WriteString(fmt.Sprintf("%d: %s", a.StatusCode, a.Message))
|
||||
_, _ = fmt.Fprintf(msg, "%d: %s", a.StatusCode, a.Message)
|
||||
|
||||
for k, v := range a.Errors {
|
||||
msg.WriteString(fmt.Sprintf(" %s: %v", k, v))
|
||||
_, _ = fmt.Fprintf(msg, " %s: %v", k, v)
|
||||
}
|
||||
|
||||
return msg.String()
|
||||
|
|
|
|||
216
providers/dns/leaseweb/internal/client.go
Normal file
216
providers/dns/leaseweb/internal/client.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/internal/errutils"
|
||||
"github.com/go-acme/lego/v5/internal/useragent"
|
||||
)
|
||||
|
||||
const defaultBaseURL = "https://api.leaseweb.com/hosting/v2"
|
||||
|
||||
const AuthHeader = "X-LSW-Auth"
|
||||
|
||||
// Client the Leaseweb API client.
|
||||
type Client struct {
|
||||
apiKey string
|
||||
|
||||
BaseURL *url.URL
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new Client.
|
||||
func NewClient(apiKey string) (*Client, error) {
|
||||
if apiKey == "" {
|
||||
return nil, errors.New("credentials missing")
|
||||
}
|
||||
|
||||
baseURL, _ := url.Parse(defaultBaseURL)
|
||||
|
||||
return &Client{
|
||||
apiKey: apiKey,
|
||||
BaseURL: baseURL,
|
||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateRRSet creates a resource record set.
|
||||
// https://developer.leaseweb.com/docs/#tag/DNS/operation/createResourceRecordSet
|
||||
func (c *Client) CreateRRSet(ctx context.Context, domainName string, rrset RRSet) (*RRSet, error) {
|
||||
endpoint := c.BaseURL.JoinPath("domains", domainName, "resourceRecordSets")
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, rrset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &RRSet{}
|
||||
|
||||
err = c.do(req, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetRRSet gets a resource record set.
|
||||
// https://developer.leaseweb.com/docs/#tag/DNS/operation/getResourceRecordSet
|
||||
func (c *Client) GetRRSet(ctx context.Context, domainName, name, rType string) (*RRSet, error) {
|
||||
endpoint := c.BaseURL.JoinPath("domains", domainName, "resourceRecordSets", name, rType)
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &RRSet{}
|
||||
|
||||
err = c.do(req, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UpdateRRSet updates a resource record set.
|
||||
// https://developer.leaseweb.com/docs/#tag/DNS/operation/updateResourceRecordSet
|
||||
func (c *Client) UpdateRRSet(ctx context.Context, domainName string, rrset RRSet) (*RRSet, error) {
|
||||
endpoint := c.BaseURL.JoinPath("domains", domainName, "resourceRecordSets", rrset.Name, rrset.Type)
|
||||
|
||||
// Reset values that are not allowed to be updated.
|
||||
rrset.Name = ""
|
||||
rrset.Type = ""
|
||||
rrset.Editable = false
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodPut, endpoint, rrset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &RRSet{}
|
||||
|
||||
err = c.do(req, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeleteRRSet deletes a resource record set.
|
||||
// https://developer.leaseweb.com/docs/#tag/DNS/operation/deleteResourceRecordSet
|
||||
func (c *Client) DeleteRRSet(ctx context.Context, domainName, name, rType string) error {
|
||||
endpoint := c.BaseURL.JoinPath("domains", domainName, "resourceRecordSets", name, rType)
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.do(req, nil)
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request, result any) error {
|
||||
useragent.SetHeader(req.Header)
|
||||
|
||||
req.Header.Add(AuthHeader, c.apiKey)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return errutils.NewHTTPDoError(req, err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
return parseError(req, resp)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errutils.NewReadResponseError(req, resp.StatusCode, err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(raw, result)
|
||||
if err != nil {
|
||||
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if payload != nil {
|
||||
err := json.NewEncoder(buf).Encode(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request JSON body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
if payload != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func parseError(req *http.Request, resp *http.Response) error {
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var errAPI APIError
|
||||
|
||||
err := json.Unmarshal(raw, &errAPI)
|
||||
if err != nil {
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return &NotFoundError{APIError{
|
||||
CorrelationID: resp.Header.Get("Correlation-Id"),
|
||||
ErrorCode: strconv.Itoa(http.StatusNotFound),
|
||||
ErrorMessage: string(raw),
|
||||
}}
|
||||
}
|
||||
|
||||
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
|
||||
}
|
||||
|
||||
if errAPI.ErrorCode == strconv.Itoa(http.StatusNotFound) {
|
||||
return &NotFoundError{APIError: errAPI}
|
||||
}
|
||||
|
||||
return &errAPI
|
||||
}
|
||||
|
||||
// TTLRounder rounds the given TTL in seconds to the next accepted value.
|
||||
// Accepted TTL values are: 60, 300, 1800, 3600, 14400, 28800, 43200, 86400.
|
||||
func TTLRounder(ttl int) int {
|
||||
for _, validTTL := range []int{60, 300, 1800, 3600, 14400, 28800, 43200, 86400} {
|
||||
if ttl <= validTTL {
|
||||
return validTTL
|
||||
}
|
||||
}
|
||||
|
||||
return 3600
|
||||
}
|
||||
149
providers/dns/leaseweb/internal/client_test.go
Normal file
149
providers/dns/leaseweb/internal/client_test.go
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v5/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func mockBuilder() *servermock.Builder[*Client] {
|
||||
return servermock.NewBuilder[*Client](
|
||||
func(server *httptest.Server) (*Client, error) {
|
||||
client, err := NewClient("secret")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.BaseURL, _ = url.Parse(server.URL)
|
||||
client.HTTPClient = server.Client()
|
||||
|
||||
return client, nil
|
||||
},
|
||||
servermock.CheckHeader().
|
||||
WithJSONHeaders().
|
||||
With(AuthHeader, "secret"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestClient_CreateRRSet(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("POST /domains/example.com/resourceRecordSets",
|
||||
servermock.ResponseFromFixture("createResourceRecordSet.json"),
|
||||
servermock.CheckRequestJSONBodyFromFixture("createResourceRecordSet-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
rrset := RRSet{
|
||||
Content: []string{"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"},
|
||||
Name: "_acme-challenge.example.com.",
|
||||
TTL: 300,
|
||||
Type: "TXT",
|
||||
}
|
||||
|
||||
result, err := client.CreateRRSet(t.Context(), "example.com", rrset)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &RRSet{
|
||||
Content: []string{"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"},
|
||||
Name: "_acme-challenge.example.com.",
|
||||
Editable: true,
|
||||
TTL: 300,
|
||||
Type: "TXT",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestClient_GetRRSet(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT",
|
||||
servermock.ResponseFromFixture("getResourceRecordSet.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
result, err := client.GetRRSet(t.Context(), "example.com", "_acme-challenge.example.com.", "TXT")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &RRSet{
|
||||
Content: []string{"foo", "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo"},
|
||||
Name: "_acme-challenge.example.com.",
|
||||
Editable: true,
|
||||
TTL: 3600,
|
||||
Type: "TXT",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestClient_GetRRSet_error_404(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT",
|
||||
servermock.ResponseFromFixture("error_404.json").
|
||||
WithStatusCode(http.StatusNotFound),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
_, err := client.GetRRSet(t.Context(), "example.com", "_acme-challenge.example.com.", "TXT")
|
||||
require.EqualError(t, err, "404: Resource not found (289346a1-3eaf-4da4-b707-62ef12eb08be)")
|
||||
|
||||
target := &NotFoundError{}
|
||||
require.ErrorAs(t, err, &target)
|
||||
}
|
||||
|
||||
func TestClient_UpdateRRSet(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("PUT /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT",
|
||||
servermock.ResponseFromFixture("updateResourceRecordSet.json"),
|
||||
servermock.CheckRequestJSONBodyFromFixture("updateResourceRecordSet-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
rrset := RRSet{
|
||||
Content: []string{"foo", "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"},
|
||||
Name: "_acme-challenge.example.com.",
|
||||
TTL: 3600,
|
||||
Type: "TXT",
|
||||
}
|
||||
|
||||
result, err := client.UpdateRRSet(t.Context(), "example.com", rrset)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &RRSet{
|
||||
Content: []string{"foo", "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"},
|
||||
Name: "_acme-challenge.example.com.",
|
||||
Editable: true,
|
||||
TTL: 3600,
|
||||
Type: "TXT",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestClient_DeleteRRSet(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("DELETE /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT",
|
||||
servermock.Noop().
|
||||
WithStatusCode(http.StatusNoContent),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := client.DeleteRRSet(t.Context(), "example.com", "_acme-challenge.example.com.", "TXT")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_DeleteRRSet_error(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("DELETE /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT",
|
||||
servermock.ResponseFromFixture("error_401.json").
|
||||
WithStatusCode(http.StatusUnauthorized),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := client.DeleteRRSet(t.Context(), "example.com", "_acme-challenge.example.com.", "TXT")
|
||||
require.EqualError(t, err, "401: You are not authorized to view this resource. (289346a1-3eaf-4da4-b707-62ef12eb08be)")
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"content": [
|
||||
"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"
|
||||
],
|
||||
"name": "_acme-challenge.example.com.",
|
||||
"ttl": 300,
|
||||
"type": "TXT"
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT"
|
||||
},
|
||||
"collection": {
|
||||
"href": "/domains/example.com/resourceRecordSets"
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"
|
||||
],
|
||||
"editable": true,
|
||||
"name": "_acme-challenge.example.com.",
|
||||
"ttl": 300,
|
||||
"type": "TXT"
|
||||
}
|
||||
6
providers/dns/leaseweb/internal/fixtures/error_400.json
Normal file
6
providers/dns/leaseweb/internal/fixtures/error_400.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"correlationId": "289346a1-3eaf-4da4-b707-62ef12eb08be",
|
||||
"errorCode": "400",
|
||||
"errorDetails": {},
|
||||
"errorMessage": "The API could not interpret your request correctly."
|
||||
}
|
||||
5
providers/dns/leaseweb/internal/fixtures/error_401.json
Normal file
5
providers/dns/leaseweb/internal/fixtures/error_401.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"correlationId": "289346a1-3eaf-4da4-b707-62ef12eb08be",
|
||||
"errorCode": "401",
|
||||
"errorMessage": "You are not authorized to view this resource."
|
||||
}
|
||||
5
providers/dns/leaseweb/internal/fixtures/error_404.json
Normal file
5
providers/dns/leaseweb/internal/fixtures/error_404.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"correlationId": "289346a1-3eaf-4da4-b707-62ef12eb08be",
|
||||
"errorCode": "404",
|
||||
"errorMessage": "Resource not found"
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT"
|
||||
},
|
||||
"collection": {
|
||||
"href": "/domains/example.com/resourceRecordSets"
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
"foo",
|
||||
"Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo"
|
||||
],
|
||||
"editable": true,
|
||||
"name": "_acme-challenge.example.com.",
|
||||
"ttl": 3600,
|
||||
"type": "TXT"
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT"
|
||||
},
|
||||
"collection": {
|
||||
"href": "/domains/example.com/resourceRecordSets"
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
"Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo"
|
||||
],
|
||||
"editable": true,
|
||||
"name": "_acme-challenge.example.com.",
|
||||
"ttl": 3600,
|
||||
"type": "TXT"
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"content": [
|
||||
"foo",
|
||||
"Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo",
|
||||
"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"
|
||||
],
|
||||
"ttl": 3600
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"content": [
|
||||
"foo"
|
||||
],
|
||||
"ttl": 3600
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT"
|
||||
},
|
||||
"collection": {
|
||||
"href": "/domains/example.com/resourceRecordSets"
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
"foo",
|
||||
"Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo",
|
||||
"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"
|
||||
],
|
||||
"editable": true,
|
||||
"name": "_acme-challenge.example.com.",
|
||||
"ttl": 3600,
|
||||
"type": "TXT"
|
||||
}
|
||||
35
providers/dns/leaseweb/internal/types.go
Normal file
35
providers/dns/leaseweb/internal/types.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type NotFoundError struct {
|
||||
APIError
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
CorrelationID string `json:"correlationId,omitempty"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
ErrorDetails json.RawMessage `json:"errorDetails,omitempty"`
|
||||
}
|
||||
|
||||
func (a *APIError) Error() string {
|
||||
msg := fmt.Sprintf("%s: %s (%s)", a.ErrorCode, a.ErrorMessage, a.CorrelationID)
|
||||
|
||||
if len(a.ErrorDetails) > 0 {
|
||||
msg += fmt.Sprintf(": %s", string(a.ErrorDetails))
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
type RRSet struct {
|
||||
Content []string `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Editable bool `json:"editable,omitempty"`
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
183
providers/dns/leaseweb/leaseweb.go
Normal file
183
providers/dns/leaseweb/leaseweb.go
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
// Package leaseweb implements a DNS provider for solving the DNS-01 challenge using Leaseweb.
|
||||
package leaseweb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/challenge/dns01"
|
||||
"github.com/go-acme/lego/v5/platform/config/env"
|
||||
"github.com/go-acme/lego/v5/providers/dns/internal/clientdebug"
|
||||
"github.com/go-acme/lego/v5/providers/dns/leaseweb/internal"
|
||||
)
|
||||
|
||||
// Environment variables names.
|
||||
const (
|
||||
envNamespace = "LEASEWEB_"
|
||||
|
||||
EnvAPIKey = envNamespace + "API_KEY"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
// Config is used to configure the creation of the DNSProvider.
|
||||
type Config struct {
|
||||
APIKey string
|
||||
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
TTL int
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DNSProvider implements the challenge.Provider interface.
|
||||
type DNSProvider struct {
|
||||
config *Config
|
||||
client *internal.Client
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for Leaseweb.
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvAPIKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("leaseweb: %w", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.APIKey = values[EnvAPIKey]
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// NewDNSProviderConfig return a DNSProvider instance configured for Leaseweb.
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("leaseweb: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
client, err := internal.NewClient(config.APIKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("leaseweb: %w", err)
|
||||
}
|
||||
|
||||
if config.HTTPClient != nil {
|
||||
client.HTTPClient = config.HTTPClient
|
||||
}
|
||||
|
||||
client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
|
||||
|
||||
return &DNSProvider{
|
||||
config: config,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters.
|
||||
func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.DefaultClient().FindZoneByFqdn(ctx, info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("leaseweb: could not find zone for domain %q: %w", domain, err)
|
||||
}
|
||||
|
||||
existingRRSet, err := d.client.GetRRSet(ctx, dns01.UnFqdn(authZone), info.EffectiveFQDN, "TXT")
|
||||
if err != nil {
|
||||
notfoundErr := &internal.NotFoundError{}
|
||||
if !errors.As(err, ¬foundErr) {
|
||||
return fmt.Errorf("leaseweb: get RRSet: %w", err)
|
||||
}
|
||||
|
||||
// Create the RRSet.
|
||||
|
||||
rrset := internal.RRSet{
|
||||
Content: []string{info.Value},
|
||||
Name: info.EffectiveFQDN,
|
||||
TTL: internal.TTLRounder(d.config.TTL),
|
||||
Type: "TXT",
|
||||
}
|
||||
|
||||
_, err = d.client.CreateRRSet(ctx, dns01.UnFqdn(authZone), rrset)
|
||||
if err != nil {
|
||||
return fmt.Errorf("leaseweb: create RRSet: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update the RRSet.
|
||||
|
||||
existingRRSet.Content = append(existingRRSet.Content, info.Value)
|
||||
|
||||
_, err = d.client.UpdateRRSet(ctx, dns01.UnFqdn(authZone), *existingRRSet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("leaseweb: update RRSet: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters.
|
||||
func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.DefaultClient().FindZoneByFqdn(ctx, info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("leaseweb: could not find zone for domain %q: %w", domain, err)
|
||||
}
|
||||
|
||||
existingRRSet, err := d.client.GetRRSet(ctx, dns01.UnFqdn(authZone), info.EffectiveFQDN, "TXT")
|
||||
if err != nil {
|
||||
return fmt.Errorf("leaseweb: get RRSet: %w", err)
|
||||
}
|
||||
|
||||
var content []string
|
||||
|
||||
for _, s := range existingRRSet.Content {
|
||||
if s != info.Value {
|
||||
content = append(content, s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(content) == 0 {
|
||||
err = d.client.DeleteRRSet(ctx, dns01.UnFqdn(authZone), info.EffectiveFQDN, "TXT")
|
||||
if err != nil {
|
||||
return fmt.Errorf("leaseweb: delete RRSet: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
existingRRSet.Content = content
|
||||
|
||||
_, err = d.client.UpdateRRSet(ctx, dns01.UnFqdn(authZone), *existingRRSet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("leaseweb: update RRSet: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||
// Adjusting here to cope with spikes in propagation times.
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
22
providers/dns/leaseweb/leaseweb.toml
Normal file
22
providers/dns/leaseweb/leaseweb.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
Name = "Leaseweb"
|
||||
Description = ''''''
|
||||
URL = "https://www.leaseweb.com/en/"
|
||||
Code = "leaseweb"
|
||||
Since = "v4.32.0"
|
||||
|
||||
Example = '''
|
||||
LEASEWEB_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
|
||||
lego --dns leaseweb -d '*.example.com' -d example.com run
|
||||
'''
|
||||
|
||||
[Configuration]
|
||||
[Configuration.Credentials]
|
||||
LEASEWEB_API_KEY = "API key"
|
||||
[Configuration.Additional]
|
||||
LEASEWEB_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
|
||||
LEASEWEB_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
|
||||
LEASEWEB_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
|
||||
LEASEWEB_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
|
||||
|
||||
[Links]
|
||||
API = "https://developer.leaseweb.com/docs/#tag/DNS"
|
||||
204
providers/dns/leaseweb/leaseweb_test.go
Normal file
204
providers/dns/leaseweb/leaseweb_test.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package leaseweb
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v5/platform/tester"
|
||||
"github.com/go-acme/lego/v5/platform/tester/servermock"
|
||||
"github.com/go-acme/lego/v5/providers/dns/leaseweb/internal"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const envDomain = envNamespace + "DOMAIN"
|
||||
|
||||
var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)
|
||||
|
||||
func TestNewDNSProvider(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
envVars: map[string]string{
|
||||
EnvAPIKey: "secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
envVars: map[string]string{},
|
||||
expected: "leaseweb: some credentials information are missing: LEASEWEB_API_KEY",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
defer envTest.RestoreEnv()
|
||||
|
||||
envTest.ClearEnv()
|
||||
|
||||
envTest.Apply(test.envVars)
|
||||
|
||||
p, err := NewDNSProvider()
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDNSProviderConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
apiKey string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
expected: "leaseweb: credentials missing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
config := NewDefaultConfig()
|
||||
config.APIKey = test.apiKey
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLivePresent(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.Present(t.Context(), envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveCleanUp(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.CleanUp(t.Context(), envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func mockBuilder() *servermock.Builder[*DNSProvider] {
|
||||
return servermock.NewBuilder(
|
||||
func(server *httptest.Server) (*DNSProvider, error) {
|
||||
config := NewDefaultConfig()
|
||||
config.APIKey = "secret"
|
||||
config.HTTPClient = server.Client()
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.client.BaseURL, _ = url.Parse(server.URL)
|
||||
|
||||
return p, nil
|
||||
},
|
||||
servermock.CheckHeader().
|
||||
WithJSONHeaders().
|
||||
With(internal.AuthHeader, "secret"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestDNSProvider_Present_create(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT",
|
||||
servermock.ResponseFromInternal("error_404.json").
|
||||
WithStatusCode(http.StatusNotFound),
|
||||
).
|
||||
Route("POST /domains/example.com/resourceRecordSets",
|
||||
servermock.ResponseFromInternal("createResourceRecordSet.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("createResourceRecordSet-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.Present(t.Context(), "example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_Present_update(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT",
|
||||
servermock.ResponseFromInternal("getResourceRecordSet.json"),
|
||||
).
|
||||
Route("PUT /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT",
|
||||
servermock.ResponseFromInternal("updateResourceRecordSet.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("updateResourceRecordSet-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.Present(t.Context(), "example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_CleanUp_delete(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT",
|
||||
servermock.ResponseFromInternal("getResourceRecordSet2.json"),
|
||||
).
|
||||
Route("DELETE /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT",
|
||||
servermock.Noop().
|
||||
WithStatusCode(http.StatusNoContent),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.CleanUp(t.Context(), "example.com", "abc", "1234d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_CleanUp_update(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT",
|
||||
servermock.ResponseFromInternal("getResourceRecordSet.json"),
|
||||
).
|
||||
Route("PUT /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT",
|
||||
servermock.ResponseFromInternal("updateResourceRecordSet.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("updateResourceRecordSet-request2.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.CleanUp(t.Context(), "example.com", "abc", "1234d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
@ -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{
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue