diff --git a/.github/ISSUE_TEMPLATE/new_dns_provider.yml b/.github/ISSUE_TEMPLATE/new_dns_provider.yml index cfd6e5c8c..b319bc287 100644 --- a/.github/ISSUE_TEMPLATE/new_dns_provider.yml +++ b/.github/ISSUE_TEMPLATE/new_dns_provider.yml @@ -14,9 +14,15 @@ body: required: true - label: Yes, I know that the lego maintainers don't have an account in all DNS providers in the world. required: true + + - type: checkboxes + id: pr + attributes: + label: Implementation + options: - label: Yes, I'm able to create a pull request and be able to maintain the implementation. required: false - - label: Yes, I'm able to test an implementation if someone creates a pull request to add the support of this DNS provider. + - label: Yes, I can test an implementation with the help of the maintainers if someone creates a pull request. required: false - type: dropdown diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 626d9f6e9..33ca106cc 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest env: GO_VERSION: stable - GOLANGCI_LINT_VERSION: v2.7.1 + GOLANGCI_LINT_VERSION: v2.10 HUGO_VERSION: 0.148.2 CGO_ENABLED: 0 LEGO_E2E_TESTS: CI @@ -44,13 +44,13 @@ jobs: install-only: true - name: Install Pebble - run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.8.0 + run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.9.0 - name: Install challtestsrv - run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.8.0 + run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.9.0 - name: Set up a Memcached server - uses: niden/actions-memcached@v7 + run: docker run -d --rm -p 11211:11211 memcached:1.6-alpine - name: Make run: | diff --git a/.golangci.yml b/.golangci.yml index b851169ff..b6ab51ccc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -183,6 +183,9 @@ linters: - text: "var-naming: avoid meaningless package names" linters: - revive + - text: "var-naming: avoid package names that conflict with Go standard library package names" + linters: + - revive - path: certcrypto/crypto.go text: (tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable linters: diff --git a/CHANGELOG.md b/CHANGELOG.md index a9974d550..ae73f70f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,60 @@ 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 +- Tag: [v4.31.0](https://github.com/go-acme/lego/releases/tag/v4.31.0) + +### Added + +- **[dnsprovider]** Add DNS provider for ISPConfig +- **[dnsprovider]** Add DNS Provider for ISPConfig (DDNS Module) +- **[dnsprovider]** Add DNS provider for Alwaysdata +- **[dnsprovider]** Add DNS provider for JDCloud +- **[dnsprovider]** Add DNS provider for 35.com/三五互联 +- **[dnsprovider]** f5xc: add an option to configure the domain of the server + +### Changed + +- **[lib]** feat: improve ACME error types +- **[dnsprovider,cname]** namedotcom: follow CNAME + +### Fixed + +- **[dnsprovider]** hetzner: fix compatibility with _FILE suffix +- **[dnsprovider]** gandiv5: fix API Key header + ## v4.30.1 - Release date: 2025-12-16 diff --git a/README.md b/README.md index ff9473e58..e90e94962 100644 --- a/README.md +++ b/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. [![Go Reference](https://pkg.go.dev/badge/github.com/go-acme/lego/v4.svg)](https://pkg.go.dev/github.com/go-acme/lego/v4) [![Build Status](https://github.com//go-acme/lego/workflows/Main/badge.svg?branch=master)](https://github.com//go-acme/lego/actions) @@ -24,7 +24,7 @@ So if you think that lego is worth it, please consider [donating](https://donate - Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): certificates for IP addresses - Support [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html): Renewal Information (ARI) Extension - Support [draft-ietf-acme-profiles-00](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/): Profiles Extension -- Comes with about [170 DNS providers](https://go-acme.github.io/lego/dns) +- Comes with about [180 DNS providers](https://go-acme.github.io/lego/dns) - Register with CA - Obtain certificates, both from scratch or with an existing CSR - Renew certificates @@ -56,18 +56,25 @@ Documentation is hosted live at https://go-acme.github.io/lego/. Detailed documentation is available [here](https://go-acme.github.io/lego/dns). +If your DNS provider is not supported, please open an [issue](https://github.com/go-acme/lego/issues/new?assignees=&labels=enhancement%2C+new-provider&template=new_dns_provider.yml). + + - + + + + + @@ -84,32 +91,37 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). + - + - + - + - + + + + - + + @@ -127,157 +139,167 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). - + + + - - + + + + - + - + - + - + - + - + + + + + - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + - - + - + - + - + - + +
35.com/三五互联 Active24 Akamai EdgeDNS Alibaba Cloud DNSAlibabaCloud ESA
AlibabaCloud ESA all-inklAlwaysdata Amazon Lightsail
Amazon Route 53 Anexia CloudDNSANS SafeDNSArtFiles
ArvanCloud Aurora DNSBindman Bluecat
Bluecat v2 BookMyName Brandit (deprecated) BunnyCheckdomain
Checkdomain Civo Cloud.ru CloudDNSCloudflare
Cloudflare ClouDNS CloudXNS (Deprecated) ConoHa v2ConoHa v3
ConoHa v3 Constellix Core-Networks CPanel/WHMDerak Cloud
CzechiaDDnss (DynDNS Service)Derak Cloud deSEC.io
Designate DNSaaS for Openstack Digital Ocean DirectAdmin
DNS Made Easy
DNSExit dnsHome.de DNSimple DNSPod (deprecated)EdgeCenter Efficient IP EpikExoscaleEuroDNS
ExcedoExoscale External program F5 XCfreemyip.comG-Core
freemyip.comFusionLayer NameSurferG-Core Gandi
Gandi Live DNS (v5) Gigahost.no Glesys
Go Daddy
Google Cloud Google Domains Gravity
Hetzner
Hosting.de Hosting.nl Hostinger
Hosttech
HTTP request http.net Huawei Cloud
Hurricane Electric DNS
HyperOne IBM Cloud (SoftLayer) IIJ DNS Platform Service
Infoblox
Infomaniak Internet Initiative Japan Internet.bs
INWX
Ionos Ionos Cloud IPv64ISPConfig 3
ISPConfig 3 - Dynamic DNS (DDNS) Module iwantmyname (Deprecated)JD Cloud Joker
Joohoi's ACME-DNS KeyHelp
Leaseweb Liara
Lima-City Linode (v4) Liquid Web
Loopia
LuaDNS Mail-in-a-Box ManageEngine CloudDNS
Manual
Metaname Metaregistrar mijn.host
Mittwald
myaddr.{tools,dev,io} MyDNS.jp MythicBeasts
Name.com
Namecheap Namesilo NearlyFreeSpeech.NET
Neodigit
Netcup Netlify Nicmanager
NIFCloud
Njalla Nodion NS1
Octenium
Open Telekom Cloud Oracle Cloud OVH
plesk.com
Porkbun PowerDNS Rackspace
Rain Yun/雨云
RcodeZero reg.ru Regfish
RFC2136
RimuHosting RU CENTER Sakura Cloud
Scaleway
Selectel Selectel v2 SelfHost.(de|eu)
Servercow
Shellrent Simply.com Sonic
Spaceship
Stackpath Syse Technitium
Tencent Cloud DNS
Tencent EdgeOne Timeweb CloudTodayNIC/时代互联 TransIP
UKFast SafeDNS Ultradns United-Domains Variomedia
VegaDNS
Vercel Versio.[nl|eu|uk] VinylDNS
Virtualname
VK Cloud Volcano Engine/火山引擎 Vscale
Vultr
webnames.ca webnames.ru Websupport
WEDOS
West.cn/西部数码 Yandex 360 Yandex Cloud
Yandex PDD
Zone.ee ZoneEdit Zonomi
diff --git a/acme/api/identifier.go b/acme/api/identifier.go index 42a8fd391..245ed8515 100644 --- a/acme/api/identifier.go +++ b/acme/api/identifier.go @@ -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]. diff --git a/acme/api/internal/sender/sender.go b/acme/api/internal/sender/sender.go index d5db5d410..d8859edf4 100644 --- a/acme/api/internal/sender/sender.go +++ b/acme/api/internal/sender/sender.go @@ -120,39 +120,46 @@ func (d *Doer) formatUserAgent() string { } func checkError(req *http.Request, resp *http.Response) error { - if resp.StatusCode >= http.StatusBadRequest { - body, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err) - } - - var errorDetails *acme.ProblemDetails - - err = json.Unmarshal(body, &errorDetails) - if err != nil { - return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body)) - } - - errorDetails.Method = req.Method - errorDetails.URL = req.URL.String() - - if errorDetails.HTTPStatus == 0 { - errorDetails.HTTPStatus = resp.StatusCode - } - - // Check for errors we handle specifically - if errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr { - return &acme.NonceError{ProblemDetails: errorDetails} - } - - if errorDetails.HTTPStatus == http.StatusConflict && errorDetails.Type == acme.AlreadyReplacedErr { - return &acme.AlreadyReplacedError{ProblemDetails: errorDetails} - } - - return errorDetails + if resp.StatusCode < http.StatusBadRequest { + return nil } - return nil + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err) + } + + var errorDetails *acme.ProblemDetails + + err = json.Unmarshal(body, &errorDetails) + if err != nil { + return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body)) + } + + errorDetails.Method = req.Method + errorDetails.URL = req.URL.String() + + if errorDetails.HTTPStatus == 0 { + errorDetails.HTTPStatus = resp.StatusCode + } + + // Check for errors we handle specifically + switch { + case errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr: + return &acme.NonceError{ProblemDetails: errorDetails} + + case errorDetails.HTTPStatus == http.StatusConflict && errorDetails.Type == acme.AlreadyReplacedErr: + return &acme.AlreadyReplacedError{ProblemDetails: errorDetails} + + case errorDetails.HTTPStatus == http.StatusTooManyRequests && errorDetails.Type == acme.RateLimitedErr: + return &acme.RateLimitedError{ + ProblemDetails: errorDetails, + RetryAfter: resp.Header.Get("Retry-After"), + } + + default: + return errorDetails + } } type httpsOnly struct { diff --git a/acme/api/internal/sender/sender_test.go b/acme/api/internal/sender/sender_test.go index 1f25c6d26..73701ab11 100644 --- a/acme/api/internal/sender/sender_test.go +++ b/acme/api/internal/sender/sender_test.go @@ -1,11 +1,14 @@ package sender import ( + "bytes" + "io" "net/http" "net/http/httptest" "strings" "testing" + "github.com/go-acme/lego/v4/acme" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -78,3 +81,70 @@ func TestDo_failWithHTTP(t *testing.T) { _, err := sender.Post(server.URL, strings.NewReader("data"), "text/plain", nil) require.ErrorContains(t, err, "HTTPS is required: http://") } + +func Test_checkError(t *testing.T) { + testCases := []struct { + desc string + resp *http.Response + assert func(t *testing.T, err error) + }{ + { + desc: "default", + resp: &http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:example","detail":"message","status":404}`)), + }, + assert: errorAs[*acme.ProblemDetails], + }, + { + desc: "badNonce", + resp: &http.Response{ + StatusCode: http.StatusBadRequest, + Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:badNonce","detail":"message","status":400}`)), + }, + assert: errorAs[*acme.NonceError], + }, + { + desc: "alreadyReplaced", + resp: &http.Response{ + StatusCode: http.StatusConflict, + Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:alreadyReplaced","detail":"message","status":409}`)), + }, + assert: errorAs[*acme.AlreadyReplacedError], + }, + { + desc: "rateLimited", + resp: &http.Response{ + StatusCode: http.StatusConflict, + Header: http.Header{ + "Retry-After": []string{"1"}, + }, + Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:rateLimited","detail":"message","status":429}`)), + }, + assert: errorAs[*acme.RateLimitedError], + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "https://example.com", nil) + + err := checkError(req, test.resp) + require.Error(t, err) + + pb := &acme.ProblemDetails{} + assert.ErrorAs(t, err, &pb) + + test.assert(t, err) + }) + } +} + +func errorAs[T error](t *testing.T, err error) { + t.Helper() + + var zero T + assert.ErrorAs(t, err, &zero) +} diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index e4ff20980..51a1b4770 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -4,10 +4,10 @@ package sender const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "xenolf-acme/4.30.1" + 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 // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) diff --git a/acme/api/service.go b/acme/api/service.go index 65518e1d9..22ce05124 100644 --- a/acme/api/service.go +++ b/acme/api/service.go @@ -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) +} diff --git a/acme/api/service_test.go b/acme/api/service_test.go index 2dbd795c9..57ea45708 100644 --- a/acme/api/service_test.go +++ b/acme/api/service_test.go @@ -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) + }) + } +} diff --git a/acme/errors.go b/acme/errors.go index 161a47c38..cd447d7b4 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -10,6 +10,7 @@ const ( errNS = "urn:ietf:params:acme:error:" BadNonceErr = errNS + "badNonce" AlreadyReplacedErr = errNS + "alreadyReplaced" + RateLimitedErr = errNS + "rateLimited" ) // ProblemDetails the problem details object. @@ -28,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 != "" { @@ -63,9 +64,30 @@ type NonceError struct { *ProblemDetails } +func (e *NonceError) Unwrap() error { + return e.ProblemDetails +} + // AlreadyReplacedError represents the error which is returned -// If the Server rejects the request because the identified certificate has already been marked as replaced. +// if the Server rejects the request because the identified certificate has already been marked as replaced. // - https://www.rfc-editor.org/rfc/rfc9773.html#section-5 type AlreadyReplacedError struct { *ProblemDetails } + +func (e *AlreadyReplacedError) Unwrap() error { + return e.ProblemDetails +} + +// RateLimitedError represents the error which is returned +// if the server rejects the request because the client has exceeded the rate limit. +// - https://www.rfc-editor.org/rfc/rfc8555.html#section-6.6 +type RateLimitedError struct { + *ProblemDetails + + RetryAfter string +} + +func (e *RateLimitedError) Unwrap() error { + return e.ProblemDetails +} diff --git a/certcrypto/crypto.go b/certcrypto/crypto.go index 00f0654b9..800bb3f5b 100644 --- a/certcrypto/crypto.go +++ b/certcrypto/crypto.go @@ -242,15 +242,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") } @@ -258,7 +258,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 { diff --git a/certificate/renewal.go b/certificate/renewal.go index 15e804745..59d31cfb5 100644 --- a/certificate/renewal.go +++ b/certificate/renewal.go @@ -11,6 +11,7 @@ import ( "time" "github.com/go-acme/lego/v4/acme" + "github.com/go-acme/lego/v4/acme/api" ) // RenewalInfoRequest contains the necessary renewal information. @@ -92,9 +93,9 @@ func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse } 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) } } diff --git a/certificate/renewal_test.go b/certificate/renewal_test.go index 6ce43e0aa..23209638a 100644 --- a/certificate/renewal_test.go +++ b/certificate/renewal_test.go @@ -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(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) diff --git a/challenge/resolver/errors.go b/challenge/resolver/errors.go index 6a859922c..65a6ccdb7 100644 --- a/challenge/resolver/errors.go +++ b/challenge/resolver/errors.go @@ -3,6 +3,8 @@ package resolver import ( "bytes" "fmt" + "maps" + "slices" "sort" ) @@ -25,3 +27,7 @@ func (e obtainError) Error() string { return buffer.String() } + +func (e obtainError) Unwrap() []error { + return slices.AppendSeq(make([]error, 0, len(e)), maps.Values(e)) +} diff --git a/challenge/resolver/errors_test.go b/challenge/resolver/errors_test.go new file mode 100644 index 000000000..d4ab3c481 --- /dev/null +++ b/challenge/resolver/errors_test.go @@ -0,0 +1,70 @@ +package resolver + +import ( + "errors" + "testing" + + "github.com/go-acme/lego/v4/acme" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_obtainError_Error(t *testing.T) { + err := obtainError{ + "a": &acme.ProblemDetails{Type: "001"}, + "b": errors.New("oops"), + "c": errors.New("I did it again"), + } + + require.EqualError(t, err, `error: one or more domains had a problem: +[a] acme: error: 0 :: 001 :: +[b] oops +[c] I did it again +`) +} + +func Test_obtainError_Unwrap(t *testing.T) { + testCases := []struct { + desc string + err obtainError + assert assert.BoolAssertionFunc + }{ + { + desc: "one ok", + err: obtainError{ + "a": &acme.ProblemDetails{}, + "b": errors.New("oops"), + "c": errors.New("I did it again"), + }, + assert: assert.True, + }, + { + desc: "all ok", + err: obtainError{ + "a": &acme.ProblemDetails{Type: "001"}, + "b": &acme.ProblemDetails{Type: "002"}, + "c": &acme.ProblemDetails{Type: "002"}, + }, + assert: assert.True, + }, + { + desc: "nope", + err: obtainError{ + "a": errors.New("hello"), + "b": errors.New("oops"), + "c": errors.New("I did it again"), + }, + assert: assert.False, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var pd *acme.ProblemDetails + + test.assert(t, errors.As(test.err, &pd)) + }) + } +} diff --git a/challenge/resolver/prober.go b/challenge/resolver/prober.go index aac1016d8..66b12c7a7 100644 --- a/challenge/resolver/prober.go +++ b/challenge/resolver/prober.go @@ -98,11 +98,24 @@ func (p *Prober) Solve(authorizations []acme.Authorization) error { } func sequentialSolve(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.Infof("acme: duplicate token for %q (DNS-01); skipping pre-solve.", authSolver.authz.Identifier.Value) + continue + } + err := solvr.PreSolve(authSolver.authz) if err != nil { failures[domain] = err @@ -111,6 +124,8 @@ func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) { continue } + + uniq[authSolver.authz.Identifier.Value+chlg.Token] = struct{}{} } // Solve challenge @@ -123,22 +138,43 @@ func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) { continue } - // Clean challenge - cleanUp(authSolver.solver, authSolver.authz) + if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok || chlg.Token == "" { + // Clean challenge + cleanUp(authSolver.solver, authSolver.authz) - if len(authSolvers)-1 > i { - solvr := authSolver.solver.(sequential) - _, interval := solvr.Sequential() - log.Infof("sequence: wait for %s", interval) - time.Sleep(interval) + if len(authSolvers)-1 > i { + solvr := authSolver.solver.(sequential) + _, interval := solvr.Sequential() + log.Infof("sequence: wait for %s", interval) + time.Sleep(interval) + } + + delete(uniq, authSolver.authz.Identifier.Value+chlg.Token) + } else { + log.Infof("acme: duplicate token for %q (DNS-01); skipping cleanup.", authSolver.authz.Identifier.Value) } } } func parallelSolve(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.Infof("acme: duplicate token for %q (DNS-01); skipping pre-solve.", authSolver.authz.Identifier.Value) + continue + } + + uniq[authz.Identifier.Value+chlg.Token] = struct{}{} + } + if solvr, ok := authSolver.solver.(preSolver); ok { err := solvr.PreSolve(authz) if err != nil { @@ -150,6 +186,16 @@ func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) { 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.Infof("acme: duplicate token for %q (DNS-01); skipping cleanup.", authSolver.authz.Identifier.Value) + continue + } + } + cleanUp(authSolver.solver, authSolver.authz) } }() diff --git a/challenge/resolver/prober_mock_test.go b/challenge/resolver/prober_mock_test.go index 5a91fe075..dc7ad8dec 100644 --- a/challenge/resolver/prober_mock_test.go +++ b/challenge/resolver/prober_mock_test.go @@ -1,6 +1,7 @@ package resolver import ( + "fmt" "time" "github.com/go-acme/lego/v4/acme" @@ -11,34 +12,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(authorization acme.Authorization) error { + s.preSolveCounter++ + return s.preSolve[authorization.Identifier.Value] } func (s *preSolverMock) Solve(authorization acme.Authorization) error { + s.solveCounter++ + return s.solve[authorization.Identifier.Value] } func (s *preSolverMock) CleanUp(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, } } diff --git a/challenge/resolver/prober_test.go b/challenge/resolver/prober_test.go index 06ff07d2c..829b16883 100644 --- a/challenge/resolver/prober_test.go +++ b/challenge/resolver/prober_test.go @@ -2,19 +2,22 @@ package resolver import ( "errors" + "fmt" "testing" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/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)) + } }) } } diff --git a/challenge/resolver/solver_manager.go b/challenge/resolver/solver_manager.go index 48d9194b9..87cf6e2d8 100644 --- a/challenge/resolver/solver_manager.go +++ b/challenge/resolver/solver_manager.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "sort" - "strconv" "time" "github.com/cenkalti/backoff/v5" @@ -94,22 +93,20 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error { 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 - ctx := context.Background() 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. @@ -134,7 +131,7 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error { return wait.Retry(ctx, operation, backoff.WithBackOff(bo), - backoff.WithMaxElapsedTime(100*initialInterval)) + backoff.WithMaxElapsedTime(100*retryAfter)) } func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) { diff --git a/cmd/cmd_list.go b/cmd/cmd_list.go index 483592d47..53cd12c3c 100644 --- a/cmd/cmd_list.go +++ b/cmd/cmd_list.go @@ -3,6 +3,7 @@ package cmd import ( "encoding/json" "fmt" + "net" "net/url" "os" "path/filepath" @@ -100,6 +101,11 @@ func listCertificates(ctx *cli.Context) error { } else { fmt.Println(" Certificate Name:", name) fmt.Println(" Domains:", strings.Join(pCert.DNSNames, ", ")) + + if len(pCert.IPAddresses) > 0 { + fmt.Println(" IPs:", formatIPAddresses(pCert.IPAddresses)) + } + fmt.Println(" Expiry Date:", pCert.NotAfter) fmt.Println(" Certificate Path:", filename) fmt.Println() @@ -150,3 +156,12 @@ func listAccount(ctx *cli.Context) error { return nil } + +func formatIPAddresses(ipAddresses []net.IP) string { + var ips []string + for _, ip := range ipAddresses { + ips = append(ips, ip.String()) + } + + return strings.Join(ips, ", ") +} diff --git a/cmd/cmd_run.go b/cmd/cmd_run.go index 16814b4de..5924c4b66 100644 --- a/cmd/cmd_run.go +++ b/cmd/cmd_run.go @@ -104,9 +104,9 @@ Your account credentials have been saved in your configuration directory at "%s". You should make a secure backup of this folder now. This -configuration directory will also contain certificates and -private keys obtained from the ACME server so making regular -backups of this folder is ideal. +configuration directory will also contain private keys +generated by lego and certificates obtained from the ACME +server. Making regular backups of this folder is ideal. ` func run(ctx *cli.Context) error { diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index d5963a601..cf9ad00ef 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.30.1+dev-release" +const defaultVersion = "v4.32.0+dev-detach" var version = "" diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index e62c337ff..f73f3920b 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -17,7 +17,9 @@ func allDNSCodes() string { "alidns", "aliesa", "allinkl", + "alwaysdata", "anexia", + "artfiles", "arvancloud", "auroradns", "autodns", @@ -30,6 +32,7 @@ func allDNSCodes() string { "binarylane", "bindman", "bluecat", + "bluecatv2", "bookmyname", "brandit", "bunny", @@ -40,16 +43,20 @@ func allDNSCodes() string { "cloudns", "cloudru", "cloudxns", + "com35", "conoha", "conohav3", "constellix", "corenetworks", "cpanel", + "czechia", + "ddnss", "derak", "desec", "designate", "digitalocean", "directadmin", + "dnsexit", "dnshomede", "dnsimple", "dnsmadeeasy", @@ -67,6 +74,8 @@ func allDNSCodes() string { "edgeone", "efficientip", "epik", + "eurodns", + "excedo", "exec", "exoscale", "f5xc", @@ -100,9 +109,13 @@ func allDNSCodes() string { "ionos", "ionoscloud", "ipv64", + "ispconfig", + "ispconfigddns", "iwantmyname", + "jdcloud", "joker", "keyhelp", + "leaseweb", "liara", "lightsail", "limacity", @@ -123,6 +136,7 @@ func allDNSCodes() string { "namecheap", "namedotcom", "namesilo", + "namesurfer", "nearlyfreespeech", "neodigit", "netcup", @@ -164,6 +178,7 @@ func allDNSCodes() string { "technitium", "tencentcloud", "timewebcloud", + "todaynic", "transip", "ultradns", "uniteddomains", @@ -254,8 +269,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() @@ -304,6 +321,27 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/allinkl`) + case "alwaysdata": + // generated from: providers/dns/alwaysdata/alwaysdata.toml + ew.writeln(`Configuration for Alwaysdata.`) + ew.writeln(`Code: 'alwaysdata'`) + ew.writeln(`Since: 'v4.31.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "ALWAYSDATA_API_KEY": API Key`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "ALWAYSDATA_ACCOUNT": Account name`) + ew.writeln(` - "ALWAYSDATA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "ALWAYSDATA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "ALWAYSDATA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "ALWAYSDATA_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/alwaysdata`) + case "anexia": // generated from: providers/dns/anexia/anexia.toml ew.writeln(`Configuration for Anexia CloudDNS.`) @@ -325,6 +363,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.`) @@ -594,6 +653,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.`) @@ -812,6 +896,27 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudxns`) + case "com35": + // generated from: providers/dns/com35/com35.toml + ew.writeln(`Configuration for 35.com/三五互联.`) + ew.writeln(`Code: 'com35'`) + ew.writeln(`Since: 'v4.31.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "COM35_PASSWORD": API password`) + ew.writeln(` - "COM35_USERNAME": Username`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "COM35_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "COM35_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) + ew.writeln(` - "COM35_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) + ew.writeln(` - "COM35_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/com35`) + case "conoha": // generated from: providers/dns/conoha/conoha.toml ew.writeln(`Configuration for ConoHa v2.`) @@ -924,6 +1029,47 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cpanel`) + case "czechia": + // generated from: providers/dns/czechia/czechia.toml + ew.writeln(`Configuration for Czechia.`) + ew.writeln(`Code: 'czechia'`) + ew.writeln(`Since: 'v4.33.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "CZECHIA_TOKEN": Authorization token`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "CZECHIA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "CZECHIA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "CZECHIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "CZECHIA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/czechia`) + + case "ddnss": + // generated from: providers/dns/ddnss/ddnss.toml + ew.writeln(`Configuration for DDnss (DynDNS Service).`) + ew.writeln(`Code: 'ddnss'`) + ew.writeln(`Since: 'v4.32.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "DDNSS_KEY": Update key`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "DDNSS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "DDNSS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "DDNSS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "DDNSS_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) + ew.writeln(` - "DDNSS_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/ddnss`) + case "derak": // generated from: providers/dns/derak/derak.toml ew.writeln(`Configuration for Derak Cloud.`) @@ -1039,6 +1185,26 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/directadmin`) + case "dnsexit": + // generated from: providers/dns/dnsexit/dnsexit.toml + ew.writeln(`Configuration for DNSExit.`) + ew.writeln(`Code: 'dnsexit'`) + ew.writeln(`Since: 'v4.32.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "DNSEXIT_API_KEY": API key`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "DNSEXIT_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "DNSEXIT_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) + ew.writeln(` - "DNSEXIT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) + ew.writeln(` - "DNSEXIT_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/dnsexit`) + case "dnshomede": // generated from: providers/dns/dnshomede/dnshomede.toml ew.writeln(`Configuration for dnsHome.de.`) @@ -1398,6 +1564,48 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/epik`) + case "eurodns": + // generated from: providers/dns/eurodns/eurodns.toml + ew.writeln(`Configuration for EuroDNS.`) + ew.writeln(`Code: 'eurodns'`) + ew.writeln(`Since: 'v4.33.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "EURODNS_API_KEY": API key`) + ew.writeln(` - "EURODNS_APP_ID": Application ID`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "EURODNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "EURODNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "EURODNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "EURODNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/eurodns`) + + case "excedo": + // generated from: providers/dns/excedo/excedo.toml + ew.writeln(`Configuration for Excedo.`) + ew.writeln(`Code: 'excedo'`) + ew.writeln(`Since: 'v4.33.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "EXCEDO_API_KEY": API key`) + ew.writeln(` - "EXCEDO_API_URL": API base URL`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "EXCEDO_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "EXCEDO_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) + ew.writeln(` - "EXCEDO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) + ew.writeln(` - "EXCEDO_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/excedo`) + case "exec": // generated from: providers/dns/exec/exec.toml ew.writeln(`Configuration for External program.`) @@ -1447,6 +1655,7 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(` - "F5XC_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) ew.writeln(` - "F5XC_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) ew.writeln(` - "F5XC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "F5XC_SERVER": Server domain (Default: console.ves.volterra.io)`) ew.writeln(` - "F5XC_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) ew.writeln() @@ -2083,6 +2292,50 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ipv64`) + case "ispconfig": + // generated from: providers/dns/ispconfig/ispconfig.toml + ew.writeln(`Configuration for ISPConfig 3.`) + ew.writeln(`Code: 'ispconfig'`) + ew.writeln(`Since: 'v4.31.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "ISPCONFIG_PASSWORD": Password`) + ew.writeln(` - "ISPCONFIG_SERVER_URL": Server URL`) + ew.writeln(` - "ISPCONFIG_USERNAME": Username`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "ISPCONFIG_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "ISPCONFIG_INSECURE_SKIP_VERIFY": Whether to verify the API certificate`) + ew.writeln(` - "ISPCONFIG_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "ISPCONFIG_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "ISPCONFIG_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/ispconfig`) + + case "ispconfigddns": + // generated from: providers/dns/ispconfigddns/ispconfigddns.toml + ew.writeln(`Configuration for ISPConfig 3 - Dynamic DNS (DDNS) Module.`) + ew.writeln(`Code: 'ispconfigddns'`) + ew.writeln(`Since: 'v4.31.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "ISPCONFIG_DDNS_SERVER_URL": API server URL (ex: https://panel.example.com:8080)`) + ew.writeln(` - "ISPCONFIG_DDNS_TOKEN": DDNS API token`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "ISPCONFIG_DDNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "ISPCONFIG_DDNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "ISPCONFIG_DDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "ISPCONFIG_DDNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/ispconfigddns`) + case "iwantmyname": // generated from: providers/dns/iwantmyname/iwantmyname.toml ew.writeln(`Configuration for iwantmyname (Deprecated).`) @@ -2104,6 +2357,28 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/iwantmyname`) + case "jdcloud": + // generated from: providers/dns/jdcloud/jdcloud.toml + ew.writeln(`Configuration for JD Cloud.`) + ew.writeln(`Code: 'jdcloud'`) + ew.writeln(`Since: 'v4.31.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "JDCLOUD_ACCESS_KEY_ID": Access key ID`) + ew.writeln(` - "JDCLOUD_ACCESS_KEY_SECRET": Access key secret`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "JDCLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "JDCLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "JDCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "JDCLOUD_REGION_ID": Region ID (Default: cn-north-1)`) + ew.writeln(` - "JDCLOUD_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/jdcloud`) + case "joker": // generated from: providers/dns/joker/joker.toml ew.writeln(`Configuration for Joker.`) @@ -2149,6 +2424,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.`) @@ -2164,6 +2459,7 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(` - "LIARA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) ew.writeln(` - "LIARA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) ew.writeln(` - "LIARA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "LIARA_TEAM_ID": The team ID to access services in a team`) ew.writeln(` - "LIARA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) ew.writeln() @@ -2556,6 +2852,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.`) @@ -3119,7 +3439,7 @@ func displayDNSHelp(w io.Writer, name string) error { case "safedns": // generated from: providers/dns/safedns/safedns.toml - ew.writeln(`Configuration for UKFast SafeDNS.`) + ew.writeln(`Configuration for ANS SafeDNS.`) ew.writeln(`Code: 'safedns'`) ew.writeln(`Since: 'v4.6.0'`) ew.writeln() @@ -3460,6 +3780,27 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/timewebcloud`) + case "todaynic": + // generated from: providers/dns/todaynic/todaynic.toml + ew.writeln(`Configuration for TodayNIC/时代互联.`) + ew.writeln(`Code: 'todaynic'`) + ew.writeln(`Since: 'v4.32.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "TODAYNIC_API_KEY": API key`) + ew.writeln(` - "TODAYNIC_AUTH_USER_ID": account ID`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "TODAYNIC_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "TODAYNIC_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "TODAYNIC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "TODAYNIC_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/todaynic`) + case "transip": // generated from: providers/dns/transip/transip.toml ew.writeln(`Configuration for TransIP.`) diff --git a/docs/content/_index.md b/docs/content/_index.md index d3787cf19..95e411afc 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -24,7 +24,7 @@ I've been maintaining it for about 10 years. - Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): issues certificates for IP addresses - Support [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html): Renewal Information (ARI) Extension - Support [draft-ietf-acme-profiles-00](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/): Profiles Extension -- Comes with about [170 DNS providers]({{% ref "dns" %}}) +- Comes with about [180 DNS providers]({{% ref "dns" %}}) - Register with CA - Obtain certificates, both from scratch or with an existing CSR - Renew certificates diff --git a/docs/content/dns/zz_gen_acme-dns.md b/docs/content/dns/zz_gen_acme-dns.md index cb3d24016..5564dba1b 100644 --- a/docs/content/dns/zz_gen_acme-dns.md +++ b/docs/content/dns/zz_gen_acme-dns.md @@ -28,13 +28,13 @@ Here is an example bash command using the Joohoi's ACME-DNS provider: ```bash ACME_DNS_API_BASE=http://10.0.0.8:4443 \ ACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \ -lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run +lego --dns "acme-dns" -d '*.example.com' -d example.com run # or ACME_DNS_API_BASE=http://10.0.0.8:4443 \ ACME_DNS_STORAGE_BASE_URL=http://10.10.10.10:80 \ -lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run +lego --dns "acme-dns" -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_active24.md b/docs/content/dns/zz_gen_active24.md index cadc6660c..6ec5c467a 100644 --- a/docs/content/dns/zz_gen_active24.md +++ b/docs/content/dns/zz_gen_active24.md @@ -28,7 +28,7 @@ Here is an example bash command using the Active24 provider: ```bash ACTIVE24_API_KEY="xxx" \ ACTIVE24_SECRET="yyy" \ -lego --email you@example.com --dns active24 -d '*.example.com' -d example.com run +lego --dns active24 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_alidns.md b/docs/content/dns/zz_gen_alidns.md index 7a7a36e8a..4ded782ab 100644 --- a/docs/content/dns/zz_gen_alidns.md +++ b/docs/content/dns/zz_gen_alidns.md @@ -28,13 +28,13 @@ Here is an example bash command using the Alibaba Cloud DNS provider: ```bash # Setup using instance RAM role ALICLOUD_RAM_ROLE=lego \ -lego --email you@example.com --dns alidns -d '*.example.com' -d example.com run +lego --dns alidns -d '*.example.com' -d example.com run # Or, using credentials ALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ ALICLOUD_SECRET_KEY=your-secret-key \ ALICLOUD_SECURITY_TOKEN=your-sts-token \ -lego --email you@example.com --dns alidns - -d '*.example.com' -d example.com run +lego --dns alidns - -d '*.example.com' -d example.com run ``` @@ -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. diff --git a/docs/content/dns/zz_gen_aliesa.md b/docs/content/dns/zz_gen_aliesa.md index b286a718a..af28f9a4e 100644 --- a/docs/content/dns/zz_gen_aliesa.md +++ b/docs/content/dns/zz_gen_aliesa.md @@ -28,13 +28,13 @@ Here is an example bash command using the AlibabaCloud ESA provider: ```bash # Setup using instance RAM role ALIESA_RAM_ROLE=lego \ -lego --email you@example.com --dns aliesa -d '*.example.com' -d example.com run +lego --dns aliesa -d '*.example.com' -d example.com run # Or, using credentials ALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ ALIESA_SECRET_KEY=your-secret-key \ ALIESA_SECURITY_TOKEN=your-sts-token \ -lego --email you@example.com --dns aliesa - -d '*.example.com' -d example.com run +lego --dns aliesa - -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_allinkl.md b/docs/content/dns/zz_gen_allinkl.md index 2415c812f..2db6ae2c5 100644 --- a/docs/content/dns/zz_gen_allinkl.md +++ b/docs/content/dns/zz_gen_allinkl.md @@ -28,7 +28,7 @@ Here is an example bash command using the all-inkl provider: ```bash ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ -lego --email you@example.com --dns allinkl -d '*.example.com' -d example.com run +lego --dns allinkl -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_alwaysdata.md b/docs/content/dns/zz_gen_alwaysdata.md new file mode 100644 index 000000000..6ec332d16 --- /dev/null +++ b/docs/content/dns/zz_gen_alwaysdata.md @@ -0,0 +1,68 @@ +--- +title: "Alwaysdata" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: alwaysdata +dnsprovider: + since: "v4.31.0" + code: "alwaysdata" + url: "https://alwaysdata.com/" +--- + + + + + + +Configuration for [Alwaysdata](https://alwaysdata.com/). + + + + +- Code: `alwaysdata` +- Since: v4.31.0 + + +Here is an example bash command using the Alwaysdata provider: + +```bash +ALWAYSDATA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns alwaysdata -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `ALWAYSDATA_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 | +|--------------------------------|-------------| +| `ALWAYSDATA_ACCOUNT` | Account name | +| `ALWAYSDATA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `ALWAYSDATA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `ALWAYSDATA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `ALWAYSDATA_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://help.alwaysdata.com/en/api/resources/) + + + + diff --git a/docs/content/dns/zz_gen_anexia.md b/docs/content/dns/zz_gen_anexia.md index 4256d957c..e12ec7cfd 100644 --- a/docs/content/dns/zz_gen_anexia.md +++ b/docs/content/dns/zz_gen_anexia.md @@ -27,7 +27,7 @@ Here is an example bash command using the Anexia CloudDNS provider: ```bash ANEXIA_TOKEN=xxx \ -lego --email you@example.com --dns anexia -d '*.example.com' -d example.com run +lego --dns anexia -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_artfiles.md b/docs/content/dns/zz_gen_artfiles.md new file mode 100644 index 000000000..15ac2d964 --- /dev/null +++ b/docs/content/dns/zz_gen_artfiles.md @@ -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/" +--- + + + + + + +Configuration for [ArtFiles](https://www.artfiles.de/extras/domains/). + + + + +- 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) + + + + diff --git a/docs/content/dns/zz_gen_arvancloud.md b/docs/content/dns/zz_gen_arvancloud.md index b9fa1af8d..96d495f71 100644 --- a/docs/content/dns/zz_gen_arvancloud.md +++ b/docs/content/dns/zz_gen_arvancloud.md @@ -27,7 +27,7 @@ Here is an example bash command using the ArvanCloud provider: ```bash ARVANCLOUD_API_KEY="Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \ -lego --email you@example.com --dns arvancloud -d '*.example.com' -d example.com run +lego --dns arvancloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_auroradns.md b/docs/content/dns/zz_gen_auroradns.md index 9fffe34bc..d608c85bb 100644 --- a/docs/content/dns/zz_gen_auroradns.md +++ b/docs/content/dns/zz_gen_auroradns.md @@ -28,7 +28,7 @@ Here is an example bash command using the Aurora DNS provider: ```bash AURORA_API_KEY=xxxxx \ AURORA_SECRET=yyyyyy \ -lego --email you@example.com --dns auroradns -d '*.example.com' -d example.com run +lego --dns auroradns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_autodns.md b/docs/content/dns/zz_gen_autodns.md index 73f41b980..f1f25e916 100644 --- a/docs/content/dns/zz_gen_autodns.md +++ b/docs/content/dns/zz_gen_autodns.md @@ -28,7 +28,7 @@ Here is an example bash command using the Autodns provider: ```bash AUTODNS_API_USER=username \ AUTODNS_API_PASSWORD=supersecretpassword \ -lego --email you@example.com --dns autodns -d '*.example.com' -d example.com run +lego --dns autodns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_axelname.md b/docs/content/dns/zz_gen_axelname.md index b1bb3e166..91476e521 100644 --- a/docs/content/dns/zz_gen_axelname.md +++ b/docs/content/dns/zz_gen_axelname.md @@ -28,7 +28,7 @@ Here is an example bash command using the Axelname provider: ```bash AXELNAME_NICKNAME="yyy" \ AXELNAME_TOKEN="xxx" \ -lego --email you@example.com --dns axelname -d '*.example.com' -d example.com run +lego --dns axelname -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_azion.md b/docs/content/dns/zz_gen_azion.md index af2a281b0..c5ca33552 100644 --- a/docs/content/dns/zz_gen_azion.md +++ b/docs/content/dns/zz_gen_azion.md @@ -27,7 +27,7 @@ Here is an example bash command using the Azion provider: ```bash AZION_PERSONAL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns azion -d '*.example.com' -d example.com run +lego --dns azion -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_azuredns.md b/docs/content/dns/zz_gen_azuredns.md index 85feaae88..3b2586711 100644 --- a/docs/content/dns/zz_gen_azuredns.md +++ b/docs/content/dns/zz_gen_azuredns.md @@ -31,32 +31,32 @@ Here is an example bash command using the Azure DNS provider: AZURE_CLIENT_ID= \ AZURE_TENANT_ID= \ AZURE_CLIENT_SECRET= \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using client certificate AZURE_CLIENT_ID= \ AZURE_TENANT_ID= \ AZURE_CLIENT_CERTIFICATE_PATH= \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using Azure CLI az login \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using Managed Identity (Azure VM) AZURE_TENANT_ID= \ AZURE_RESOURCE_GROUP= \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using Managed Identity (Azure Arc) AZURE_TENANT_ID= \ IMDS_ENDPOINT=http://localhost:40342 \ IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_baiducloud.md b/docs/content/dns/zz_gen_baiducloud.md index 9f59aa156..59a2f9a2d 100644 --- a/docs/content/dns/zz_gen_baiducloud.md +++ b/docs/content/dns/zz_gen_baiducloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the Baidu Cloud provider: ```bash BAIDUCLOUD_ACCESS_KEY_ID="xxx" \ BAIDUCLOUD_SECRET_ACCESS_KEY="yyy" \ -lego --email you@example.com --dns baiducloud -d '*.example.com' -d example.com run +lego --dns baiducloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_beget.md b/docs/content/dns/zz_gen_beget.md index ae1d16a7c..3f03a2ac5 100644 --- a/docs/content/dns/zz_gen_beget.md +++ b/docs/content/dns/zz_gen_beget.md @@ -28,7 +28,7 @@ Here is an example bash command using the Beget.com provider: ```bash BEGET_USERNAME=xxxxxx \ BEGET_PASSWORD=yyyyyy \ -lego --email you@example.com --dns beget -d '*.example.com' -d example.com run +lego --dns beget -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_binarylane.md b/docs/content/dns/zz_gen_binarylane.md index 4d65bb0bc..eebf3c54e 100644 --- a/docs/content/dns/zz_gen_binarylane.md +++ b/docs/content/dns/zz_gen_binarylane.md @@ -27,7 +27,7 @@ Here is an example bash command using the Binary Lane provider: ```bash BINARYLANE_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns binarylane -d '*.example.com' -d example.com run +lego --dns binarylane -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_bindman.md b/docs/content/dns/zz_gen_bindman.md index e12f25b7a..fcceb8962 100644 --- a/docs/content/dns/zz_gen_bindman.md +++ b/docs/content/dns/zz_gen_bindman.md @@ -27,7 +27,7 @@ Here is an example bash command using the Bindman provider: ```bash BINDMAN_MANAGER_ADDRESS= \ -lego --email you@example.com --dns bindman -d '*.example.com' -d example.com run +lego --dns bindman -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_bluecat.md b/docs/content/dns/zz_gen_bluecat.md index ee45c7f8b..2d9eb5b48 100644 --- a/docs/content/dns/zz_gen_bluecat.md +++ b/docs/content/dns/zz_gen_bluecat.md @@ -32,7 +32,7 @@ BLUECAT_USER_NAME=myusername \ BLUECAT_CONFIG_NAME=myconfig \ BLUECAT_SERVER_URL=https://bam.example.com \ BLUECAT_TTL=30 \ -lego --email you@example.com --dns bluecat -d '*.example.com' -d example.com run +lego --dns bluecat -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_bluecatv2.md b/docs/content/dns/zz_gen_bluecatv2.md new file mode 100644 index 000000000..7d748df99 --- /dev/null +++ b/docs/content/dns/zz_gen_bluecatv2.md @@ -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" +--- + + + + + + +Configuration for [Bluecat v2](https://www.bluecatnetworks.com). + + + + +- 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) + + + + diff --git a/docs/content/dns/zz_gen_bookmyname.md b/docs/content/dns/zz_gen_bookmyname.md index 3f5d1f2c3..cb7e1d3a1 100644 --- a/docs/content/dns/zz_gen_bookmyname.md +++ b/docs/content/dns/zz_gen_bookmyname.md @@ -28,7 +28,7 @@ Here is an example bash command using the BookMyName provider: ```bash BOOKMYNAME_USERNAME="xxx" \ BOOKMYNAME_PASSWORD="yyy" \ -lego --email you@example.com --dns bookmyname -d '*.example.com' -d example.com run +lego --dns bookmyname -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_brandit.md b/docs/content/dns/zz_gen_brandit.md index 5d1f91214..fdb538684 100644 --- a/docs/content/dns/zz_gen_brandit.md +++ b/docs/content/dns/zz_gen_brandit.md @@ -31,7 +31,7 @@ Here is an example bash command using the Brandit (deprecated) provider: ```bash BRANDIT_API_KEY=xxxxxxxxxxxxxxxxxxxxx \ BRANDIT_API_USERNAME=yyyyyyyyyyyyyyyyyyyy \ -lego --email you@example.com --dns brandit -d '*.example.com' -d example.com run +lego --dns brandit -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_bunny.md b/docs/content/dns/zz_gen_bunny.md index 884c61aea..63c30782a 100644 --- a/docs/content/dns/zz_gen_bunny.md +++ b/docs/content/dns/zz_gen_bunny.md @@ -27,7 +27,7 @@ Here is an example bash command using the Bunny provider: ```bash BUNNY_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ -lego --email you@example.com --dns bunny -d '*.example.com' -d example.com run +lego --dns bunny -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_checkdomain.md b/docs/content/dns/zz_gen_checkdomain.md index 516d87880..e0275f6c9 100644 --- a/docs/content/dns/zz_gen_checkdomain.md +++ b/docs/content/dns/zz_gen_checkdomain.md @@ -27,7 +27,7 @@ Here is an example bash command using the Checkdomain provider: ```bash CHECKDOMAIN_TOKEN=yoursecrettoken \ -lego --email you@example.com --dns checkdomain -d '*.example.com' -d example.com run +lego --dns checkdomain -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_civo.md b/docs/content/dns/zz_gen_civo.md index a2cffe27c..61303b539 100644 --- a/docs/content/dns/zz_gen_civo.md +++ b/docs/content/dns/zz_gen_civo.md @@ -27,7 +27,7 @@ Here is an example bash command using the Civo provider: ```bash CIVO_TOKEN=xxxxxx \ -lego --email you@example.com --dns civo -d '*.example.com' -d example.com run +lego --dns civo -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_clouddns.md b/docs/content/dns/zz_gen_clouddns.md index 27a254873..d10d1d6a1 100644 --- a/docs/content/dns/zz_gen_clouddns.md +++ b/docs/content/dns/zz_gen_clouddns.md @@ -29,7 +29,7 @@ Here is an example bash command using the CloudDNS provider: CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \ CLOUDDNS_EMAIL=you@example.com \ CLOUDDNS_PASSWORD=b9841238feb177a84330f \ -lego --email you@example.com --dns clouddns -d '*.example.com' -d example.com run +lego --dns clouddns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_cloudflare.md b/docs/content/dns/zz_gen_cloudflare.md index 0fd1d440e..f3390a5fd 100644 --- a/docs/content/dns/zz_gen_cloudflare.md +++ b/docs/content/dns/zz_gen_cloudflare.md @@ -28,12 +28,12 @@ Here is an example bash command using the Cloudflare provider: ```bash CLOUDFLARE_EMAIL=you@example.com \ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ -lego --email you@example.com --dns cloudflare -d '*.example.com' -d example.com run +lego --dns cloudflare -d '*.example.com' -d example.com run # or CLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --email you@example.com --dns cloudflare -d '*.example.com' -d example.com run +lego --dns cloudflare -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_cloudns.md b/docs/content/dns/zz_gen_cloudns.md index 01d4b7815..26bd838f2 100644 --- a/docs/content/dns/zz_gen_cloudns.md +++ b/docs/content/dns/zz_gen_cloudns.md @@ -28,7 +28,7 @@ Here is an example bash command using the ClouDNS provider: ```bash CLOUDNS_AUTH_ID=xxxx \ CLOUDNS_AUTH_PASSWORD=yyyy \ -lego --email you@example.com --dns cloudns -d '*.example.com' -d example.com run +lego --dns cloudns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_cloudru.md b/docs/content/dns/zz_gen_cloudru.md index 52190b031..6dc3b0030 100644 --- a/docs/content/dns/zz_gen_cloudru.md +++ b/docs/content/dns/zz_gen_cloudru.md @@ -29,7 +29,7 @@ Here is an example bash command using the Cloud.ru provider: CLOUDRU_SERVICE_INSTANCE_ID=ppp \ CLOUDRU_KEY_ID=xxx \ CLOUDRU_SECRET=yyy \ -lego --email you@example.com --dns cloudru -d '*.example.com' -d example.com run +lego --dns cloudru -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_cloudxns.md b/docs/content/dns/zz_gen_cloudxns.md index 0b290b693..b26e5ddb5 100644 --- a/docs/content/dns/zz_gen_cloudxns.md +++ b/docs/content/dns/zz_gen_cloudxns.md @@ -28,7 +28,7 @@ Here is an example bash command using the CloudXNS (Deprecated) provider: ```bash CLOUDXNS_API_KEY=xxxx \ CLOUDXNS_SECRET_KEY=yyyy \ -lego --email you@example.com --dns cloudxns -d '*.example.com' -d example.com run +lego --dns cloudxns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_com35.md b/docs/content/dns/zz_gen_com35.md new file mode 100644 index 000000000..e2552e57c --- /dev/null +++ b/docs/content/dns/zz_gen_com35.md @@ -0,0 +1,69 @@ +--- +title: "35.com/三五互联" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: com35 +dnsprovider: + since: "v4.31.0" + code: "com35" + url: "https://www.35.cn/" +--- + + + + + + +Configuration for [35.com/三五互联](https://www.35.cn/). + + + + +- Code: `com35` +- Since: v4.31.0 + + +Here is an example bash command using the 35.com/三五互联 provider: + +```bash +COM35_USERNAME="xxx" \ +COM35_PASSWORD="yyy" \ +lego --dns com35 -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `COM35_PASSWORD` | API password | +| `COM35_USERNAME` | 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 | +|--------------------------------|-------------| +| `COM35_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `COM35_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | +| `COM35_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | +| `COM35_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + + + +## More information + +- [API documentation](https://api.35.cn/CustomerCenter/doc/domain_v2.html) + + + + diff --git a/docs/content/dns/zz_gen_conoha.md b/docs/content/dns/zz_gen_conoha.md index 4d5f84660..08a979b31 100644 --- a/docs/content/dns/zz_gen_conoha.md +++ b/docs/content/dns/zz_gen_conoha.md @@ -29,7 +29,7 @@ Here is an example bash command using the ConoHa v2 provider: CONOHA_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ CONOHA_API_USERNAME=xxxx \ CONOHA_API_PASSWORD=yyyy \ -lego --email you@example.com --dns conoha -d '*.example.com' -d example.com run +lego --dns conoha -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_conohav3.md b/docs/content/dns/zz_gen_conohav3.md index 208f2f91b..e473f9434 100644 --- a/docs/content/dns/zz_gen_conohav3.md +++ b/docs/content/dns/zz_gen_conohav3.md @@ -29,7 +29,7 @@ Here is an example bash command using the ConoHa v3 provider: CONOHAV3_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ CONOHAV3_API_USER_ID=xxxx \ CONOHAV3_API_PASSWORD=yyyy \ -lego --email you@example.com --dns conohav3 -d '*.example.com' -d example.com run +lego --dns conohav3 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_constellix.md b/docs/content/dns/zz_gen_constellix.md index 23628e001..d4ce02bac 100644 --- a/docs/content/dns/zz_gen_constellix.md +++ b/docs/content/dns/zz_gen_constellix.md @@ -28,7 +28,7 @@ Here is an example bash command using the Constellix provider: ```bash CONSTELLIX_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ CONSTELLIX_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ -lego --email you@example.com --dns constellix -d '*.example.com' -d example.com run +lego --dns constellix -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_corenetworks.md b/docs/content/dns/zz_gen_corenetworks.md index dc756647e..05468b1a3 100644 --- a/docs/content/dns/zz_gen_corenetworks.md +++ b/docs/content/dns/zz_gen_corenetworks.md @@ -28,7 +28,7 @@ Here is an example bash command using the Core-Networks provider: ```bash CORENETWORKS_LOGIN="xxxx" \ CORENETWORKS_PASSWORD="yyyy" \ -lego --email you@example.com --dns corenetworks -d '*.example.com' -d example.com run +lego --dns corenetworks -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_cpanel.md b/docs/content/dns/zz_gen_cpanel.md index 48cb229e7..e5c0cc047 100644 --- a/docs/content/dns/zz_gen_cpanel.md +++ b/docs/content/dns/zz_gen_cpanel.md @@ -31,7 +31,7 @@ Here is an example bash command using the CPanel/WHM provider: CPANEL_USERNAME="yyyy" \ CPANEL_TOKEN="xxxx" \ CPANEL_BASE_URL="https://example.com:2083" \ -lego --email you@example.com --dns cpanel -d '*.example.com' -d example.com run +lego --dns cpanel -d '*.example.com' -d example.com run ## WHM @@ -39,7 +39,7 @@ CPANEL_MODE=whm \ CPANEL_USERNAME="yyyy" \ CPANEL_TOKEN="xxxx" \ CPANEL_BASE_URL="https://example.com:2087" \ -lego --email you@example.com --dns cpanel -d '*.example.com' -d example.com run +lego --dns cpanel -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_czechia.md b/docs/content/dns/zz_gen_czechia.md new file mode 100644 index 000000000..7b1cdd1ae --- /dev/null +++ b/docs/content/dns/zz_gen_czechia.md @@ -0,0 +1,67 @@ +--- +title: "Czechia" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: czechia +dnsprovider: + since: "v4.33.0" + code: "czechia" + url: "https://www.czechia.com/" +--- + + + + + + +Configuration for [Czechia](https://www.czechia.com/). + + + + +- Code: `czechia` +- Since: v4.33.0 + + +Here is an example bash command using the Czechia provider: + +```bash +CZECHIA_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns czechia -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `CZECHIA_TOKEN` | Authorization token | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `CZECHIA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `CZECHIA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `CZECHIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `CZECHIA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + + + +## More information + +- [API documentation](https://api.czechia.com/swagger/index.html) + + + + diff --git a/docs/content/dns/zz_gen_ddnss.md b/docs/content/dns/zz_gen_ddnss.md new file mode 100644 index 000000000..e159d58b4 --- /dev/null +++ b/docs/content/dns/zz_gen_ddnss.md @@ -0,0 +1,68 @@ +--- +title: "DDnss (DynDNS Service)" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: ddnss +dnsprovider: + since: "v4.32.0" + code: "ddnss" + url: "https://ddnss.de/" +--- + + + + + + +Configuration for [DDnss (DynDNS Service)](https://ddnss.de/). + + + + +- Code: `ddnss` +- Since: v4.32.0 + + +Here is an example bash command using the DDnss (DynDNS Service) provider: + +```bash +DDNSS_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns ddnss -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `DDNSS_KEY` | Update 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 | +|--------------------------------|-------------| +| `DDNSS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `DDNSS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `DDNSS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `DDNSS_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | +| `DDNSS_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://ddnss.de/info.php) + + + + diff --git a/docs/content/dns/zz_gen_derak.md b/docs/content/dns/zz_gen_derak.md index fedbf4683..c5c8c7bc6 100644 --- a/docs/content/dns/zz_gen_derak.md +++ b/docs/content/dns/zz_gen_derak.md @@ -27,7 +27,7 @@ Here is an example bash command using the Derak Cloud provider: ```bash DERAK_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns derak -d '*.example.com' -d example.com run +lego --dns derak -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_desec.md b/docs/content/dns/zz_gen_desec.md index 977a00e06..4dbc713d6 100644 --- a/docs/content/dns/zz_gen_desec.md +++ b/docs/content/dns/zz_gen_desec.md @@ -27,7 +27,7 @@ Here is an example bash command using the deSEC.io provider: ```bash DESEC_TOKEN=x-xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns desec -d '*.example.com' -d example.com run +lego --dns desec -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_designate.md b/docs/content/dns/zz_gen_designate.md index 74cd04920..9703f094d 100644 --- a/docs/content/dns/zz_gen_designate.md +++ b/docs/content/dns/zz_gen_designate.md @@ -28,7 +28,7 @@ Here is an example bash command using the Designate DNSaaS for Openstack provide ```bash # With a `clouds.yaml` OS_CLOUD=my_openstack \ -lego --email you@example.com --dns designate -d '*.example.com' -d example.com run +lego --dns designate -d '*.example.com' -d example.com run # or @@ -37,7 +37,7 @@ OS_REGION_NAME=RegionOne \ OS_PROJECT_ID=23d4522a987d4ab529f722a007c27846 OS_USERNAME=myuser \ OS_PASSWORD=passw0rd \ -lego --email you@example.com --dns designate -d '*.example.com' -d example.com run +lego --dns designate -d '*.example.com' -d example.com run # or @@ -46,7 +46,7 @@ OS_REGION_NAME=RegionOne \ OS_AUTH_TYPE=v3applicationcredential \ OS_APPLICATION_CREDENTIAL_ID=imn74uq0or7dyzz20dwo1ytls4me8dry \ OS_APPLICATION_CREDENTIAL_SECRET=68FuSPSdQqkFQYH5X1OoriEIJOwyLtQ8QSqXZOc9XxFK1A9tzZT6He2PfPw0OMja \ -lego --email you@example.com --dns designate -d '*.example.com' -d example.com run +lego --dns designate -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_digitalocean.md b/docs/content/dns/zz_gen_digitalocean.md index 24307cfb0..4dc43886d 100644 --- a/docs/content/dns/zz_gen_digitalocean.md +++ b/docs/content/dns/zz_gen_digitalocean.md @@ -27,7 +27,7 @@ Here is an example bash command using the Digital Ocean provider: ```bash DO_AUTH_TOKEN=xxxxxx \ -lego --email you@example.com --dns digitalocean -d '*.example.com' -d example.com run +lego --dns digitalocean -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_directadmin.md b/docs/content/dns/zz_gen_directadmin.md index 006cb87d6..1d03dcc4e 100644 --- a/docs/content/dns/zz_gen_directadmin.md +++ b/docs/content/dns/zz_gen_directadmin.md @@ -29,7 +29,7 @@ Here is an example bash command using the DirectAdmin provider: DIRECTADMIN_API_URL="http://example.com:2222" \ DIRECTADMIN_USERNAME=xxxx \ DIRECTADMIN_PASSWORD=yyy \ -lego --email you@example.com --dns directadmin -d '*.example.com' -d example.com run +lego --dns directadmin -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dnsexit.md b/docs/content/dns/zz_gen_dnsexit.md new file mode 100644 index 000000000..aca5357e8 --- /dev/null +++ b/docs/content/dns/zz_gen_dnsexit.md @@ -0,0 +1,67 @@ +--- +title: "DNSExit" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: dnsexit +dnsprovider: + since: "v4.32.0" + code: "dnsexit" + url: "https://dnsexit.com" +--- + + + + + + +Configuration for [DNSExit](https://dnsexit.com). + + + + +- Code: `dnsexit` +- Since: v4.32.0 + + +Here is an example bash command using the DNSExit provider: + +```bash +DNSEXIT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns dnsexit -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `DNSEXIT_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 | +|--------------------------------|-------------| +| `DNSEXIT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `DNSEXIT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | +| `DNSEXIT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | +| `DNSEXIT_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://dnsexit.com/dns/dns-api/) + + + + diff --git a/docs/content/dns/zz_gen_dnshomede.md b/docs/content/dns/zz_gen_dnshomede.md index b865578e6..ca7f83523 100644 --- a/docs/content/dns/zz_gen_dnshomede.md +++ b/docs/content/dns/zz_gen_dnshomede.md @@ -27,10 +27,10 @@ Here is an example bash command using the dnsHome.de provider: ```bash DNSHOMEDE_CREDENTIALS=example.org:password \ -lego --email you@example.com --dns dnshomede -d '*.example.com' -d example.com run +lego --dns dnshomede -d '*.example.com' -d example.com run DNSHOMEDE_CREDENTIALS=my.example.org:password1,demo.example.org:password2 \ -lego --email you@example.com --dns dnshomede -d my.example.org -d demo.example.org +lego --dns dnshomede -d my.example.org -d demo.example.org ``` diff --git a/docs/content/dns/zz_gen_dnsimple.md b/docs/content/dns/zz_gen_dnsimple.md index d73122273..7799ece88 100644 --- a/docs/content/dns/zz_gen_dnsimple.md +++ b/docs/content/dns/zz_gen_dnsimple.md @@ -27,7 +27,7 @@ Here is an example bash command using the DNSimple provider: ```bash DNSIMPLE_OAUTH_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --email you@example.com --dns dnsimple -d '*.example.com' -d example.com run +lego --dns dnsimple -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dnsmadeeasy.md b/docs/content/dns/zz_gen_dnsmadeeasy.md index 572676fbd..e7f260889 100644 --- a/docs/content/dns/zz_gen_dnsmadeeasy.md +++ b/docs/content/dns/zz_gen_dnsmadeeasy.md @@ -28,7 +28,7 @@ Here is an example bash command using the DNS Made Easy provider: ```bash DNSMADEEASY_API_KEY=xxxxxx \ DNSMADEEASY_API_SECRET=yyyyy \ -lego --email you@example.com --dns dnsmadeeasy -d '*.example.com' -d example.com run +lego --dns dnsmadeeasy -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dnspod.md b/docs/content/dns/zz_gen_dnspod.md index b9e906052..86112a5ce 100644 --- a/docs/content/dns/zz_gen_dnspod.md +++ b/docs/content/dns/zz_gen_dnspod.md @@ -27,7 +27,7 @@ Here is an example bash command using the DNSPod (deprecated) provider: ```bash DNSPOD_API_KEY=xxxxxx \ -lego --email you@example.com --dns dnspod -d '*.example.com' -d example.com run +lego --dns dnspod -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dode.md b/docs/content/dns/zz_gen_dode.md index 153650406..28eebe5fa 100644 --- a/docs/content/dns/zz_gen_dode.md +++ b/docs/content/dns/zz_gen_dode.md @@ -27,7 +27,7 @@ Here is an example bash command using the Domain Offensive (do.de) provider: ```bash DODE_TOKEN=xxxxxx \ -lego --email you@example.com --dns dode -d '*.example.com' -d example.com run +lego --dns dode -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_domeneshop.md b/docs/content/dns/zz_gen_domeneshop.md index a519cfbef..0530ab365 100644 --- a/docs/content/dns/zz_gen_domeneshop.md +++ b/docs/content/dns/zz_gen_domeneshop.md @@ -28,7 +28,7 @@ Here is an example bash command using the Domeneshop provider: ```bash DOMENESHOP_API_TOKEN= \ DOMENESHOP_API_SECRET= \ -lego --email example@example.com --dns domeneshop -d '*.example.com' -d example.com run +lego --dns domeneshop -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dreamhost.md b/docs/content/dns/zz_gen_dreamhost.md index e713b8ad2..b9d273099 100644 --- a/docs/content/dns/zz_gen_dreamhost.md +++ b/docs/content/dns/zz_gen_dreamhost.md @@ -27,7 +27,7 @@ Here is an example bash command using the DreamHost provider: ```bash DREAMHOST_API_KEY="YOURAPIKEY" \ -lego --email you@example.com --dns dreamhost -d '*.example.com' -d example.com run +lego --dns dreamhost -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_duckdns.md b/docs/content/dns/zz_gen_duckdns.md index 1290b82fd..8b60780d2 100644 --- a/docs/content/dns/zz_gen_duckdns.md +++ b/docs/content/dns/zz_gen_duckdns.md @@ -27,7 +27,7 @@ Here is an example bash command using the Duck DNS provider: ```bash DUCKDNS_TOKEN=xxxxxx \ -lego --email you@example.com --dns duckdns -d '*.example.com' -d example.com run +lego --dns duckdns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dyn.md b/docs/content/dns/zz_gen_dyn.md index f241ea930..e31a90e45 100644 --- a/docs/content/dns/zz_gen_dyn.md +++ b/docs/content/dns/zz_gen_dyn.md @@ -29,7 +29,7 @@ Here is an example bash command using the Dyn provider: DYN_CUSTOMER_NAME=xxxxxx \ DYN_USER_NAME=yyyyy \ DYN_PASSWORD=zzzz \ -lego --email you@example.com --dns dyn -d '*.example.com' -d example.com run +lego --dns dyn -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dyndnsfree.md b/docs/content/dns/zz_gen_dyndnsfree.md index 6f4cf46ff..ea549b4e2 100644 --- a/docs/content/dns/zz_gen_dyndnsfree.md +++ b/docs/content/dns/zz_gen_dyndnsfree.md @@ -28,7 +28,7 @@ Here is an example bash command using the DynDnsFree.de provider: ```bash DYNDNSFREE_USERNAME="xxx" \ DYNDNSFREE_PASSWORD="yyy" \ -lego --email you@example.com --dns dyndnsfree -d '*.example.com' -d example.com run +lego --dns dyndnsfree -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dynu.md b/docs/content/dns/zz_gen_dynu.md index 4db76456f..a1f3e762e 100644 --- a/docs/content/dns/zz_gen_dynu.md +++ b/docs/content/dns/zz_gen_dynu.md @@ -27,7 +27,7 @@ Here is an example bash command using the Dynu provider: ```bash DYNU_API_KEY=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --email you@example.com --dns dynu -d '*.example.com' -d example.com run +lego --dns dynu -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_easydns.md b/docs/content/dns/zz_gen_easydns.md index 196e6ab7c..12f69e09c 100644 --- a/docs/content/dns/zz_gen_easydns.md +++ b/docs/content/dns/zz_gen_easydns.md @@ -28,7 +28,7 @@ Here is an example bash command using the EasyDNS provider: ```bash EASYDNS_TOKEN=xxx \ EASYDNS_KEY=yyy \ -lego --email you@example.com --dns easydns -d '*.example.com' -d example.com run +lego --dns easydns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_edgecenter.md b/docs/content/dns/zz_gen_edgecenter.md index 7c7dd9379..1fd9fe5fa 100644 --- a/docs/content/dns/zz_gen_edgecenter.md +++ b/docs/content/dns/zz_gen_edgecenter.md @@ -27,7 +27,7 @@ Here is an example bash command using the EdgeCenter provider: ```bash EDGECENTER_PERMANENT_API_TOKEN=xxxxx \ -lego --email you@example.com --dns edgecenter -d '*.example.com' -d example.com run +lego --dns edgecenter -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_edgedns.md b/docs/content/dns/zz_gen_edgedns.md index 21d819d2c..31b191168 100644 --- a/docs/content/dns/zz_gen_edgedns.md +++ b/docs/content/dns/zz_gen_edgedns.md @@ -30,7 +30,7 @@ AKAMAI_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890ABCDEFG= \ AKAMAI_CLIENT_TOKEN=akab-mnbvcxzlkjhgfdsapoiuytrewq1234567 \ AKAMAI_HOST=akab-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.luna.akamaiapis.net \ AKAMAI_ACCESS_TOKEN=akab-1234567890qwerty-asdfghjklzxcvtnu \ -lego --email you@example.com --dns edgedns -d '*.example.com' -d example.com run +lego --dns edgedns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_edgeone.md b/docs/content/dns/zz_gen_edgeone.md index 227127d65..ba5de5ba2 100644 --- a/docs/content/dns/zz_gen_edgeone.md +++ b/docs/content/dns/zz_gen_edgeone.md @@ -28,7 +28,7 @@ Here is an example bash command using the Tencent EdgeOne provider: ```bash EDGEONE_SECRET_ID=abcdefghijklmnopqrstuvwx \ EDGEONE_SECRET_KEY=your-secret-key \ -lego --email you@example.com --dns edgeone -d '*.example.com' -d example.com run +lego --dns edgeone -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_efficientip.md b/docs/content/dns/zz_gen_efficientip.md index 7c151e67a..acca3ebb7 100644 --- a/docs/content/dns/zz_gen_efficientip.md +++ b/docs/content/dns/zz_gen_efficientip.md @@ -30,7 +30,7 @@ EFFICIENTIP_USERNAME="user" \ EFFICIENTIP_PASSWORD="secret" \ EFFICIENTIP_HOSTNAME="ipam.example.org" \ EFFICIENTIP_DNS_NAME="dns.smart" \ -lego --email you@example.com --dns efficientip -d '*.example.com' -d example.com run +lego --dns efficientip -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_epik.md b/docs/content/dns/zz_gen_epik.md index 50f66e8da..a7fc029d3 100644 --- a/docs/content/dns/zz_gen_epik.md +++ b/docs/content/dns/zz_gen_epik.md @@ -27,7 +27,7 @@ Here is an example bash command using the Epik provider: ```bash EPIK_SIGNATURE=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns epik -d '*.example.com' -d example.com run +lego --dns epik -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_eurodns.md b/docs/content/dns/zz_gen_eurodns.md new file mode 100644 index 000000000..cb5a0418d --- /dev/null +++ b/docs/content/dns/zz_gen_eurodns.md @@ -0,0 +1,69 @@ +--- +title: "EuroDNS" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: eurodns +dnsprovider: + since: "v4.33.0" + code: "eurodns" + url: "https://www.eurodns.com/" +--- + + + + + + +Configuration for [EuroDNS](https://www.eurodns.com/). + + + + +- Code: `eurodns` +- Since: v4.33.0 + + +Here is an example bash command using the EuroDNS provider: + +```bash +EURODNS_APP_ID="xxx" \ +EURODNS_API_KEY="yyy" \ +lego --dns eurodns -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `EURODNS_API_KEY` | API key | +| `EURODNS_APP_ID` | Application ID | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `EURODNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `EURODNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `EURODNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `EURODNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + + + +## More information + +- [API documentation](https://docapi.eurodns.com/) + + + + diff --git a/docs/content/dns/zz_gen_excedo.md b/docs/content/dns/zz_gen_excedo.md new file mode 100644 index 000000000..456e6f60a --- /dev/null +++ b/docs/content/dns/zz_gen_excedo.md @@ -0,0 +1,69 @@ +--- +title: "Excedo" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: excedo +dnsprovider: + since: "v4.33.0" + code: "excedo" + url: "https://excedo.se/" +--- + + + + + + +Configuration for [Excedo](https://excedo.se/). + + + + +- Code: `excedo` +- Since: v4.33.0 + + +Here is an example bash command using the Excedo provider: + +```bash +EXCEDO_API_KEY=your-api-key \ +EXCEDO_API_URL=your-base-url \ +lego --dns excedo -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `EXCEDO_API_KEY` | API key | +| `EXCEDO_API_URL` | API base URL | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `EXCEDO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `EXCEDO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | +| `EXCEDO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | +| `EXCEDO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + + + +## More information + +- [API documentation](none) + + + + diff --git a/docs/content/dns/zz_gen_exec.md b/docs/content/dns/zz_gen_exec.md index fb2b17e3d..ad2e6906e 100644 --- a/docs/content/dns/zz_gen_exec.md +++ b/docs/content/dns/zz_gen_exec.md @@ -26,7 +26,7 @@ Here is an example bash command using the External program provider: ```bash EXEC_PATH=/the/path/to/myscript.sh \ -lego --email you@example.com --dns exec -d '*.example.com' -d example.com run +lego --dns exec -d '*.example.com' -d example.com run ``` @@ -61,7 +61,7 @@ For example, requesting a certificate for the domain 'my.example.org' can be ach ```bash EXEC_PATH=./update-dns.sh \ -lego --email you@example.com --dns exec --d my.example.org run +lego --dns exec --d my.example.org run ``` It will then call the program './update-dns.sh' with like this: @@ -81,7 +81,7 @@ If you want to use the raw domain, token, and keyAuth values with your program, ```bash EXEC_MODE=RAW \ EXEC_PATH=./update-dns.sh \ -lego --email you@example.com --dns exec -d my.example.org run +lego --dns exec -d my.example.org run ``` It will then call the program `./update-dns.sh` like this: diff --git a/docs/content/dns/zz_gen_exoscale.md b/docs/content/dns/zz_gen_exoscale.md index 5392ff573..e599d6487 100644 --- a/docs/content/dns/zz_gen_exoscale.md +++ b/docs/content/dns/zz_gen_exoscale.md @@ -28,7 +28,7 @@ Here is an example bash command using the Exoscale provider: ```bash EXOSCALE_API_KEY=abcdefghijklmnopqrstuvwx \ EXOSCALE_API_SECRET=xxxxxxx \ -lego --email you@example.com --dns exoscale -d '*.example.com' -d example.com run +lego --dns exoscale -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_f5xc.md b/docs/content/dns/zz_gen_f5xc.md index c8a664a00..0fd8fe58a 100644 --- a/docs/content/dns/zz_gen_f5xc.md +++ b/docs/content/dns/zz_gen_f5xc.md @@ -29,7 +29,7 @@ Here is an example bash command using the F5 XC provider: F5XC_API_TOKEN="xxx" \ F5XC_TENANT_NAME="yyy" \ F5XC_GROUP_NAME="zzz" \ -lego --email you@example.com --dns f5xc -d '*.example.com' -d example.com run +lego --dns f5xc -d '*.example.com' -d example.com run ``` @@ -54,6 +54,7 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | `F5XC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | | `F5XC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | | `F5XC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `F5XC_SERVER` | Server domain (Default: console.ves.volterra.io) | | `F5XC_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. diff --git a/docs/content/dns/zz_gen_freemyip.md b/docs/content/dns/zz_gen_freemyip.md index d89e17c27..215f8eb84 100644 --- a/docs/content/dns/zz_gen_freemyip.md +++ b/docs/content/dns/zz_gen_freemyip.md @@ -27,7 +27,7 @@ Here is an example bash command using the freemyip.com provider: ```bash FREEMYIP_TOKEN=xxxxxx \ -lego --email you@example.com --dns freemyip -d '*.example.com' -d example.com run +lego --dns freemyip -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_gandi.md b/docs/content/dns/zz_gen_gandi.md index 961ed6873..b02d97819 100644 --- a/docs/content/dns/zz_gen_gandi.md +++ b/docs/content/dns/zz_gen_gandi.md @@ -27,7 +27,7 @@ Here is an example bash command using the Gandi provider: ```bash GANDI_API_KEY=abcdefghijklmnopqrstuvwx \ -lego --email you@example.com --dns gandi -d '*.example.com' -d example.com run +lego --dns gandi -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_gandiv5.md b/docs/content/dns/zz_gen_gandiv5.md index 773bd3b08..78824abbe 100644 --- a/docs/content/dns/zz_gen_gandiv5.md +++ b/docs/content/dns/zz_gen_gandiv5.md @@ -27,7 +27,7 @@ Here is an example bash command using the Gandi Live DNS (v5) provider: ```bash GANDIV5_PERSONAL_ACCESS_TOKEN=abcdefghijklmnopqrstuvwx \ -lego --email you@example.com --dns gandiv5 -d '*.example.com' -d example.com run +lego --dns gandiv5 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_gcloud.md b/docs/content/dns/zz_gen_gcloud.md index ff228a1c8..64acc1d1e 100644 --- a/docs/content/dns/zz_gen_gcloud.md +++ b/docs/content/dns/zz_gen_gcloud.md @@ -29,18 +29,18 @@ Here is an example bash command using the Google Cloud provider: # Using a service account file GCE_PROJECT="gc-project-id" \ GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \ -lego --email you@example.com --dns gcloud -d '*.example.com' -d example.com run +lego --dns gcloud -d '*.example.com' -d example.com run # Using default credentials with impersonation GCE_PROJECT="gc-project-id" \ GCE_IMPERSONATE_SERVICE_ACCOUNT="target-sa@gc-project-id.iam.gserviceaccount.com" \ -lego --email you@example.com --dns gcloud -d '*.example.com' -d example.com run +lego --dns gcloud -d '*.example.com' -d example.com run # Using service account key with impersonation GCE_PROJECT="gc-project-id" \ GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \ GCE_IMPERSONATE_SERVICE_ACCOUNT="target-sa@gc-project-id.iam.gserviceaccount.com" \ -lego --email you@example.com --dns gcloud -d '*.example.com' -d example.com run +lego --dns gcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_gcore.md b/docs/content/dns/zz_gen_gcore.md index f2a17c3fb..21a7ee9b1 100644 --- a/docs/content/dns/zz_gen_gcore.md +++ b/docs/content/dns/zz_gen_gcore.md @@ -27,7 +27,7 @@ Here is an example bash command using the G-Core provider: ```bash GCORE_PERMANENT_API_TOKEN=xxxxx \ -lego --email you@example.com --dns gcore -d '*.example.com' -d example.com run +lego --dns gcore -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_gigahostno.md b/docs/content/dns/zz_gen_gigahostno.md index afb7c64c9..a59ffc401 100644 --- a/docs/content/dns/zz_gen_gigahostno.md +++ b/docs/content/dns/zz_gen_gigahostno.md @@ -28,7 +28,7 @@ Here is an example bash command using the Gigahost.no provider: ```bash GIGAHOSTNO_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ GIGAHOSTNO_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ -lego --email you@example.com --dns gigahostno -d '*.example.com' -d example.com run +lego --dns gigahostno -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_glesys.md b/docs/content/dns/zz_gen_glesys.md index ff43cfe9a..2d2608330 100644 --- a/docs/content/dns/zz_gen_glesys.md +++ b/docs/content/dns/zz_gen_glesys.md @@ -28,7 +28,7 @@ Here is an example bash command using the Glesys provider: ```bash GLESYS_API_USER=xxxxx \ GLESYS_API_KEY=yyyyy \ -lego --email you@example.com --dns glesys -d '*.example.com' -d example.com run +lego --dns glesys -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_godaddy.md b/docs/content/dns/zz_gen_godaddy.md index c5392a878..bc51cd69b 100644 --- a/docs/content/dns/zz_gen_godaddy.md +++ b/docs/content/dns/zz_gen_godaddy.md @@ -28,7 +28,7 @@ Here is an example bash command using the Go Daddy provider: ```bash GODADDY_API_KEY=xxxxxxxx \ GODADDY_API_SECRET=yyyyyyyy \ -lego --email you@example.com --dns godaddy -d '*.example.com' -d example.com run +lego --dns godaddy -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_googledomains.md b/docs/content/dns/zz_gen_googledomains.md index c6f6d0577..2421184c0 100644 --- a/docs/content/dns/zz_gen_googledomains.md +++ b/docs/content/dns/zz_gen_googledomains.md @@ -27,7 +27,7 @@ Here is an example bash command using the Google Domains provider: ```bash GOOGLE_DOMAINS_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns googledomains -d '*.example.com' -d example.com run +lego --dns googledomains -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_gravity.md b/docs/content/dns/zz_gen_gravity.md index 42d5e6128..654ad8424 100644 --- a/docs/content/dns/zz_gen_gravity.md +++ b/docs/content/dns/zz_gen_gravity.md @@ -29,7 +29,7 @@ Here is an example bash command using the Gravity provider: GRAVITY_SERVER_URL="https://example.org:1234" \ GRAVITY_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ GRAVITY_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ -lego --email you@example.com --dns gravity -d '*.example.com' -d example.com run +lego --dns gravity -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_hetzner.md b/docs/content/dns/zz_gen_hetzner.md index 5778a64ce..4e81bd4d9 100644 --- a/docs/content/dns/zz_gen_hetzner.md +++ b/docs/content/dns/zz_gen_hetzner.md @@ -27,7 +27,7 @@ Here is an example bash command using the Hetzner provider: ```bash HETZNER_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns hetzner -d '*.example.com' -d example.com run +lego --dns hetzner -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_hostingde.md b/docs/content/dns/zz_gen_hostingde.md index cc86116e1..4a66fe0f1 100644 --- a/docs/content/dns/zz_gen_hostingde.md +++ b/docs/content/dns/zz_gen_hostingde.md @@ -27,7 +27,7 @@ Here is an example bash command using the Hosting.de provider: ```bash HOSTINGDE_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns hostingde -d '*.example.com' -d example.com run +lego --dns hostingde -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_hostinger.md b/docs/content/dns/zz_gen_hostinger.md index 193455f63..c05b3f003 100644 --- a/docs/content/dns/zz_gen_hostinger.md +++ b/docs/content/dns/zz_gen_hostinger.md @@ -27,7 +27,7 @@ Here is an example bash command using the Hostinger provider: ```bash HOSTINGER_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns hostinger -d '*.example.com' -d example.com run +lego --dns hostinger -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_hostingnl.md b/docs/content/dns/zz_gen_hostingnl.md index 0577affd4..09cb69b47 100644 --- a/docs/content/dns/zz_gen_hostingnl.md +++ b/docs/content/dns/zz_gen_hostingnl.md @@ -27,7 +27,7 @@ Here is an example bash command using the Hosting.nl provider: ```bash HOSTINGNL_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns hostingnl -d '*.example.com' -d example.com run +lego --dns hostingnl -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_hosttech.md b/docs/content/dns/zz_gen_hosttech.md index 4f9f117ba..9435cc562 100644 --- a/docs/content/dns/zz_gen_hosttech.md +++ b/docs/content/dns/zz_gen_hosttech.md @@ -27,7 +27,7 @@ Here is an example bash command using the Hosttech provider: ```bash HOSTTECH_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns hosttech -d '*.example.com' -d example.com run +lego --dns hosttech -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_httpnet.md b/docs/content/dns/zz_gen_httpnet.md index 06883b3f8..862909697 100644 --- a/docs/content/dns/zz_gen_httpnet.md +++ b/docs/content/dns/zz_gen_httpnet.md @@ -27,7 +27,7 @@ Here is an example bash command using the http.net provider: ```bash HTTPNET_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns httpnet -d '*.example.com' -d example.com run +lego --dns httpnet -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_httpreq.md b/docs/content/dns/zz_gen_httpreq.md index 9c6476802..7f6a8d576 100644 --- a/docs/content/dns/zz_gen_httpreq.md +++ b/docs/content/dns/zz_gen_httpreq.md @@ -27,7 +27,7 @@ Here is an example bash command using the HTTP request provider: ```bash HTTPREQ_ENDPOINT=http://my.server.com:9090 \ -lego --email you@example.com --dns httpreq -d '*.example.com' -d example.com run +lego --dns httpreq -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_huaweicloud.md b/docs/content/dns/zz_gen_huaweicloud.md index 9a37a8878..46d121265 100644 --- a/docs/content/dns/zz_gen_huaweicloud.md +++ b/docs/content/dns/zz_gen_huaweicloud.md @@ -29,7 +29,7 @@ Here is an example bash command using the Huawei Cloud provider: HUAWEICLOUD_ACCESS_KEY_ID=your-access-key-id \ HUAWEICLOUD_SECRET_ACCESS_KEY=your-secret-access-key \ HUAWEICLOUD_REGION=cn-south-1 \ -lego --email you@example.com --dns huaweicloud -d '*.example.com' -d example.com run +lego --dns huaweicloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_hurricane.md b/docs/content/dns/zz_gen_hurricane.md index da78630d4..0c195d19c 100644 --- a/docs/content/dns/zz_gen_hurricane.md +++ b/docs/content/dns/zz_gen_hurricane.md @@ -27,10 +27,10 @@ Here is an example bash command using the Hurricane Electric DNS provider: ```bash HURRICANE_TOKENS=example.org:token \ -lego --email you@example.com --dns hurricane -d '*.example.com' -d example.com run +lego --dns hurricane -d '*.example.com' -d example.com run HURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 \ -lego --email you@example.com --dns hurricane -d my.example.org -d demo.example.org +lego --dns hurricane -d my.example.org -d demo.example.org ``` diff --git a/docs/content/dns/zz_gen_hyperone.md b/docs/content/dns/zz_gen_hyperone.md index 83dfdb111..bc496f7bc 100644 --- a/docs/content/dns/zz_gen_hyperone.md +++ b/docs/content/dns/zz_gen_hyperone.md @@ -26,7 +26,7 @@ Configuration for [HyperOne](https://www.hyperone.com). Here is an example bash command using the HyperOne provider: ```bash -lego --email you@example.com --dns hyperone -d '*.example.com' -d example.com run +lego --dns hyperone -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ibmcloud.md b/docs/content/dns/zz_gen_ibmcloud.md index 94997b703..c5a48d2ad 100644 --- a/docs/content/dns/zz_gen_ibmcloud.md +++ b/docs/content/dns/zz_gen_ibmcloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the IBM Cloud (SoftLayer) provider: ```bash SOFTLAYER_USERNAME=xxxxx \ SOFTLAYER_API_KEY=yyyyy \ -lego --email you@example.com --dns ibmcloud -d '*.example.com' -d example.com run +lego --dns ibmcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_iij.md b/docs/content/dns/zz_gen_iij.md index 8c73f58a5..c7acfe3a0 100644 --- a/docs/content/dns/zz_gen_iij.md +++ b/docs/content/dns/zz_gen_iij.md @@ -29,7 +29,7 @@ Here is an example bash command using the Internet Initiative Japan provider: IIJ_API_ACCESS_KEY=xxxxxxxx \ IIJ_API_SECRET_KEY=yyyyyy \ IIJ_DO_SERVICE_CODE=zzzzzz \ -lego --email you@example.com --dns iij -d '*.example.com' -d example.com run +lego --dns iij -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_iijdpf.md b/docs/content/dns/zz_gen_iijdpf.md index 7c694fc32..12e126f49 100644 --- a/docs/content/dns/zz_gen_iijdpf.md +++ b/docs/content/dns/zz_gen_iijdpf.md @@ -28,7 +28,7 @@ Here is an example bash command using the IIJ DNS Platform Service provider: ```bash IIJ_DPF_API_TOKEN=xxxxxxxx \ IIJ_DPF_DPM_SERVICE_CODE=yyyyyy \ -lego --email you@example.com --dns iijdpf -d '*.example.com' -d example.com run +lego --dns iijdpf -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_infoblox.md b/docs/content/dns/zz_gen_infoblox.md index 2d07628f3..74b80b2d1 100644 --- a/docs/content/dns/zz_gen_infoblox.md +++ b/docs/content/dns/zz_gen_infoblox.md @@ -29,7 +29,7 @@ Here is an example bash command using the Infoblox provider: INFOBLOX_USERNAME=api-user-529 \ INFOBLOX_PASSWORD=b9841238feb177a84330febba8a83208921177bffe733 \ INFOBLOX_HOST=infoblox.example.org -lego --email you@example.com --dns infoblox -d '*.example.com' -d example.com run +lego --dns infoblox -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_infomaniak.md b/docs/content/dns/zz_gen_infomaniak.md index be02d8ee8..7254241b1 100644 --- a/docs/content/dns/zz_gen_infomaniak.md +++ b/docs/content/dns/zz_gen_infomaniak.md @@ -27,7 +27,7 @@ Here is an example bash command using the Infomaniak provider: ```bash INFOMANIAK_ACCESS_TOKEN=1234567898765432 \ -lego --email you@example.com --dns infomaniak -d '*.example.com' -d example.com run +lego --dns infomaniak -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_internetbs.md b/docs/content/dns/zz_gen_internetbs.md index e98fbf4b9..f0d9df3c1 100644 --- a/docs/content/dns/zz_gen_internetbs.md +++ b/docs/content/dns/zz_gen_internetbs.md @@ -28,7 +28,7 @@ Here is an example bash command using the Internet.bs provider: ```bash INTERNET_BS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ INTERNET_BS_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ -lego --email you@example.com --dns internetbs -d '*.example.com' -d example.com run +lego --dns internetbs -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_inwx.md b/docs/content/dns/zz_gen_inwx.md index a46ff061e..3e7d999e9 100644 --- a/docs/content/dns/zz_gen_inwx.md +++ b/docs/content/dns/zz_gen_inwx.md @@ -28,13 +28,13 @@ Here is an example bash command using the INWX provider: ```bash INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ -lego --email you@example.com --dns inwx -d '*.example.com' -d example.com run +lego --dns inwx -d '*.example.com' -d example.com run # 2FA INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ INWX_SHARED_SECRET=zzzzzzzzzz \ -lego --email you@example.com --dns inwx -d '*.example.com' -d example.com run +lego --dns inwx -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ionos.md b/docs/content/dns/zz_gen_ionos.md index 60a2ede03..78bd3ffb1 100644 --- a/docs/content/dns/zz_gen_ionos.md +++ b/docs/content/dns/zz_gen_ionos.md @@ -27,7 +27,7 @@ Here is an example bash command using the Ionos provider: ```bash IONOS_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns ionos -d '*.example.com' -d example.com run +lego --dns ionos -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ionoscloud.md b/docs/content/dns/zz_gen_ionoscloud.md index 9d33a95e5..6007670a7 100644 --- a/docs/content/dns/zz_gen_ionoscloud.md +++ b/docs/content/dns/zz_gen_ionoscloud.md @@ -27,7 +27,7 @@ Here is an example bash command using the Ionos Cloud provider: ```bash IONOSCLOUD_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns ionoscloud -d '*.example.com' -d example.com run +lego --dns ionoscloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ipv64.md b/docs/content/dns/zz_gen_ipv64.md index 21327caaf..00a0292a6 100644 --- a/docs/content/dns/zz_gen_ipv64.md +++ b/docs/content/dns/zz_gen_ipv64.md @@ -27,7 +27,7 @@ Here is an example bash command using the IPv64 provider: ```bash IPV64_API_KEY=xxxxxx \ -lego --email you@example.com --dns ipv64 -d '*.example.com' -d example.com run +lego --dns ipv64 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ispconfig.md b/docs/content/dns/zz_gen_ispconfig.md new file mode 100644 index 000000000..e56f1f0b1 --- /dev/null +++ b/docs/content/dns/zz_gen_ispconfig.md @@ -0,0 +1,72 @@ +--- +title: "ISPConfig 3" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: ispconfig +dnsprovider: + since: "v4.31.0" + code: "ispconfig" + url: "https://www.ispconfig.org/" +--- + + + + + + +Configuration for [ISPConfig 3](https://www.ispconfig.org/). + + + + +- Code: `ispconfig` +- Since: v4.31.0 + + +Here is an example bash command using the ISPConfig 3 provider: + +```bash +ISPCONFIG_SERVER_URL="https://example.com:8080/remote/json.php" \ +ISPCONFIG_USERNAME="xxx" \ +ISPCONFIG_PASSWORD="yyy" \ +lego --dns ispconfig -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `ISPCONFIG_PASSWORD` | Password | +| `ISPCONFIG_SERVER_URL` | Server URL | +| `ISPCONFIG_USERNAME` | 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 | +|--------------------------------|-------------| +| `ISPCONFIG_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `ISPCONFIG_INSECURE_SKIP_VERIFY` | Whether to verify the API certificate | +| `ISPCONFIG_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `ISPCONFIG_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `ISPCONFIG_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://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/index.html) + + + + diff --git a/docs/content/dns/zz_gen_ispconfigddns.md b/docs/content/dns/zz_gen_ispconfigddns.md new file mode 100644 index 000000000..3d1dd83c3 --- /dev/null +++ b/docs/content/dns/zz_gen_ispconfigddns.md @@ -0,0 +1,74 @@ +--- +title: "ISPConfig 3 - Dynamic DNS (DDNS) Module" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: ispconfigddns +dnsprovider: + since: "v4.31.0" + code: "ispconfigddns" + url: "https://www.ispconfig.org/" +--- + + + + + + +Configuration for [ISPConfig 3 - Dynamic DNS (DDNS) Module](https://www.ispconfig.org/). + + + + +- Code: `ispconfigddns` +- Since: v4.31.0 + + +Here is an example bash command using the ISPConfig 3 - Dynamic DNS (DDNS) Module provider: + +```bash +ISPCONFIG_DDNS_SERVER_URL="https://panel.example.com:8080" \ +ISPCONFIG_DDNS_TOKEN=xxxxxx \ +lego --dns ispconfigddns -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `ISPCONFIG_DDNS_SERVER_URL` | API server URL (ex: https://panel.example.com:8080) | +| `ISPCONFIG_DDNS_TOKEN` | DDNS API token | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `ISPCONFIG_DDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `ISPCONFIG_DDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `ISPCONFIG_DDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `ISPCONFIG_DDNS_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. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + +ISPConfig DNS provider supports leveraging the [ISPConfig 3 Dynamic DNS (DDNS) Module](https://github.com/mhofer117/ispconfig-ddns-module). + +Requires the DDNS module described at https://www.ispconfig.org/ispconfig/download/ + +See https://www.howtoforge.com/community/threads/ispconfig-3-danymic-dns-ddns-module.87967/ for additional details. + + + +## More information + +- [API documentation](https://github.com/mhofer117/ispconfig-ddns-module/tree/master/lib/updater) + + + + diff --git a/docs/content/dns/zz_gen_iwantmyname.md b/docs/content/dns/zz_gen_iwantmyname.md index cbdb29cb3..4638e1379 100644 --- a/docs/content/dns/zz_gen_iwantmyname.md +++ b/docs/content/dns/zz_gen_iwantmyname.md @@ -30,7 +30,7 @@ Here is an example bash command using the iwantmyname (Deprecated) provider: ```bash IWANTMYNAME_USERNAME=xxxxxxxx \ IWANTMYNAME_PASSWORD=xxxxxxxx \ -lego --email you@example.com --dns iwantmyname -d '*.example.com' -d example.com run +lego --dns iwantmyname -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_jdcloud.md b/docs/content/dns/zz_gen_jdcloud.md new file mode 100644 index 000000000..a37cc3520 --- /dev/null +++ b/docs/content/dns/zz_gen_jdcloud.md @@ -0,0 +1,71 @@ +--- +title: "JD Cloud" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: jdcloud +dnsprovider: + since: "v4.31.0" + code: "jdcloud" + url: "https://www.jdcloud.com/" +--- + + + + + + +Configuration for [JD Cloud](https://www.jdcloud.com/). + + + + +- Code: `jdcloud` +- Since: v4.31.0 + + +Here is an example bash command using the JD Cloud provider: + +```bash +JDCLOUD_ACCESS_KEY_ID="xxx" \ +JDCLOUD_ACCESS_KEY_SECRET="yyy" \ +lego --dns jdcloud -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `JDCLOUD_ACCESS_KEY_ID` | Access key ID | +| `JDCLOUD_ACCESS_KEY_SECRET` | Access key secret | + +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 | +|--------------------------------|-------------| +| `JDCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `JDCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `JDCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `JDCLOUD_REGION_ID` | Region ID (Default: cn-north-1) | +| `JDCLOUD_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.jdcloud.com/cn/jd-cloud-dns/api/overview) +- [Go client](https://github.com/jdcloud-api/jdcloud-sdk-go) + + + + diff --git a/docs/content/dns/zz_gen_joker.md b/docs/content/dns/zz_gen_joker.md index c8d55b2f7..a5ecd47de 100644 --- a/docs/content/dns/zz_gen_joker.md +++ b/docs/content/dns/zz_gen_joker.md @@ -30,17 +30,17 @@ Here is an example bash command using the Joker provider: JOKER_API_MODE=SVC \ JOKER_USERNAME= \ JOKER_PASSWORD= \ -lego --email you@example.com --dns joker -d '*.example.com' -d example.com run +lego --dns joker -d '*.example.com' -d example.com run # DMAPI JOKER_API_MODE=DMAPI \ JOKER_USERNAME= \ JOKER_PASSWORD= \ -lego --email you@example.com --dns joker -d '*.example.com' -d example.com run +lego --dns joker -d '*.example.com' -d example.com run ## or JOKER_API_MODE=DMAPI \ JOKER_API_KEY= \ -lego --email you@example.com --dns joker -d '*.example.com' -d example.com run +lego --dns joker -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_keyhelp.md b/docs/content/dns/zz_gen_keyhelp.md index 2886a0a8e..e39d3ce82 100644 --- a/docs/content/dns/zz_gen_keyhelp.md +++ b/docs/content/dns/zz_gen_keyhelp.md @@ -28,7 +28,7 @@ Here is an example bash command using the KeyHelp provider: ```bash KEYHELP_BASE_URL="https://keyhelp.example.com" \ KEYHELP_API_KEY="xxx" \ -lego --email you@example.com --dns keyhelp -d '*.example.com' -d example.com run +lego --dns keyhelp -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_leaseweb.md b/docs/content/dns/zz_gen_leaseweb.md new file mode 100644 index 000000000..13ded490a --- /dev/null +++ b/docs/content/dns/zz_gen_leaseweb.md @@ -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/" +--- + + + + + + +Configuration for [Leaseweb](https://www.leaseweb.com/en/). + + + + +- 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) + + + + diff --git a/docs/content/dns/zz_gen_liara.md b/docs/content/dns/zz_gen_liara.md index 2c3d59ae0..658ce8077 100644 --- a/docs/content/dns/zz_gen_liara.md +++ b/docs/content/dns/zz_gen_liara.md @@ -27,7 +27,7 @@ Here is an example bash command using the Liara provider: ```bash LIARA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns liara -d '*.example.com' -d example.com run +lego --dns liara -d '*.example.com' -d example.com run ``` @@ -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. diff --git a/docs/content/dns/zz_gen_limacity.md b/docs/content/dns/zz_gen_limacity.md index 2a01814e5..29bc6e0a7 100644 --- a/docs/content/dns/zz_gen_limacity.md +++ b/docs/content/dns/zz_gen_limacity.md @@ -27,7 +27,7 @@ Here is an example bash command using the Lima-City provider: ```bash LIMACITY_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns limacity -d '*.example.com' -d example.com run +lego --dns limacity -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_linode.md b/docs/content/dns/zz_gen_linode.md index 8c8487541..e41ba7cd9 100644 --- a/docs/content/dns/zz_gen_linode.md +++ b/docs/content/dns/zz_gen_linode.md @@ -27,7 +27,7 @@ Here is an example bash command using the Linode (v4) provider: ```bash LINODE_TOKEN=xxxxx \ -lego --email you@example.com --dns linode -d '*.example.com' -d example.com run +lego --dns linode -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_liquidweb.md b/docs/content/dns/zz_gen_liquidweb.md index 9d8fe8c9c..bd2ce63b6 100644 --- a/docs/content/dns/zz_gen_liquidweb.md +++ b/docs/content/dns/zz_gen_liquidweb.md @@ -28,7 +28,7 @@ Here is an example bash command using the Liquid Web provider: ```bash LWAPI_USERNAME=someuser \ LWAPI_PASSWORD=somepass \ -lego --email you@example.com --dns liquidweb -d '*.example.com' -d example.com run +lego --dns liquidweb -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_loopia.md b/docs/content/dns/zz_gen_loopia.md index 3951de8e1..bb3120c00 100644 --- a/docs/content/dns/zz_gen_loopia.md +++ b/docs/content/dns/zz_gen_loopia.md @@ -28,7 +28,7 @@ Here is an example bash command using the Loopia provider: ```bash LOOPIA_API_USER=xxxxxxxx \ LOOPIA_API_PASSWORD=yyyyyyyy \ -lego --email you@example.com --dns loopia -d '*.example.com' -d example.com run +lego --dns loopia -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_luadns.md b/docs/content/dns/zz_gen_luadns.md index c987cc9bf..8bf718ba3 100644 --- a/docs/content/dns/zz_gen_luadns.md +++ b/docs/content/dns/zz_gen_luadns.md @@ -28,7 +28,7 @@ Here is an example bash command using the LuaDNS provider: ```bash LUADNS_API_USERNAME=youremail \ LUADNS_API_TOKEN=xxxxxxxx \ -lego --email you@example.com --dns luadns -d '*.example.com' -d example.com run +lego --dns luadns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_mailinabox.md b/docs/content/dns/zz_gen_mailinabox.md index 3ffed1cc7..62a6bdb57 100644 --- a/docs/content/dns/zz_gen_mailinabox.md +++ b/docs/content/dns/zz_gen_mailinabox.md @@ -29,7 +29,7 @@ Here is an example bash command using the Mail-in-a-Box provider: MAILINABOX_EMAIL=user@example.com \ MAILINABOX_PASSWORD=yyyy \ MAILINABOX_BASE_URL=https://box.example.com \ -lego --email you@example.com --dns mailinabox -d '*.example.com' -d example.com run +lego --dns mailinabox -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_manageengine.md b/docs/content/dns/zz_gen_manageengine.md index 32b3a3aeb..a39db8208 100644 --- a/docs/content/dns/zz_gen_manageengine.md +++ b/docs/content/dns/zz_gen_manageengine.md @@ -28,7 +28,7 @@ Here is an example bash command using the ManageEngine CloudDNS provider: ```bash MANAGEENGINE_CLIENT_ID="xxx" \ MANAGEENGINE_CLIENT_SECRET="yyy" \ -lego --email you@example.com --dns manageengine -d '*.example.com' -d example.com run +lego --dns manageengine -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_manual.md b/docs/content/dns/zz_gen_manual.md index 0300d8400..832ccaf58 100644 --- a/docs/content/dns/zz_gen_manual.md +++ b/docs/content/dns/zz_gen_manual.md @@ -25,7 +25,7 @@ Solving the DNS-01 challenge using CLI prompt. Here is an example bash command using the Manual provider: ```bash -lego --email you@example.com --dns manual -d '*.example.com' -d example.com run +lego --dns manual -d '*.example.com' -d example.com run ``` @@ -36,7 +36,7 @@ lego --email you@example.com --dns manual -d '*.example.com' -d example.com run To start using the CLI prompt "provider", start lego with `--dns manual`: ```console -$ lego --email "you@example.com" --domains="example.com" --dns "manual" run +$ lego --dns manual -d example.com run ``` What follows are a few log print-outs, interspersed with some prompts, asking for you to do perform some actions: @@ -54,13 +54,13 @@ If you accept the linked Terms of Service, hit `Enter`. [INFO] acme: Registering account for you@example.com !!!! HEADS UP !!!! - Your account credentials have been saved in your Let's Encrypt - configuration directory at "./.lego/accounts". +Your account credentials have been saved in your +configuration directory at "./.lego/accounts". - You should make a secure backup of this folder now. This - configuration directory will also contain certificates and - private keys obtained from Let's Encrypt so making regular - backups of this folder is ideal. +You should make a secure backup of this folder now. This +configuration directory will also contain private keys +generated by lego and certificates obtained from the ACME +server. Making regular backups of this folder is ideal. [INFO] [example.com] acme: Obtaining bundled SAN certificate [INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/2345678901 [INFO] [example.com] acme: Could not find solver for: tls-alpn-01 diff --git a/docs/content/dns/zz_gen_metaname.md b/docs/content/dns/zz_gen_metaname.md index a90d0170b..156cf15eb 100644 --- a/docs/content/dns/zz_gen_metaname.md +++ b/docs/content/dns/zz_gen_metaname.md @@ -28,7 +28,7 @@ Here is an example bash command using the Metaname provider: ```bash METANAME_ACCOUNT_REFERENCE=xxxx \ METANAME_API_KEY=yyyyyyy \ -lego --email you@example.com --dns metaname -d '*.example.com' -d example.com run +lego --dns metaname -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_metaregistrar.md b/docs/content/dns/zz_gen_metaregistrar.md index 63cc2bebc..22de046e2 100644 --- a/docs/content/dns/zz_gen_metaregistrar.md +++ b/docs/content/dns/zz_gen_metaregistrar.md @@ -27,7 +27,7 @@ Here is an example bash command using the Metaregistrar provider: ```bash METAREGISTRAR_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns metaregistrar -d '*.example.com' -d example.com run +lego --dns metaregistrar -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_mijnhost.md b/docs/content/dns/zz_gen_mijnhost.md index 42abc6558..3d8f71aff 100644 --- a/docs/content/dns/zz_gen_mijnhost.md +++ b/docs/content/dns/zz_gen_mijnhost.md @@ -27,7 +27,7 @@ Here is an example bash command using the mijn.host provider: ```bash MIJNHOST_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns mijnhost -d '*.example.com' -d example.com run +lego --dns mijnhost -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_mittwald.md b/docs/content/dns/zz_gen_mittwald.md index 943397ee9..7714ef54f 100644 --- a/docs/content/dns/zz_gen_mittwald.md +++ b/docs/content/dns/zz_gen_mittwald.md @@ -27,7 +27,7 @@ Here is an example bash command using the Mittwald provider: ```bash MITTWALD_TOKEN=my-token \ -lego --email you@example.com --dns mittwald -d '*.example.com' -d example.com run +lego --dns mittwald -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_myaddr.md b/docs/content/dns/zz_gen_myaddr.md index 277a0bf06..4a52a058b 100644 --- a/docs/content/dns/zz_gen_myaddr.md +++ b/docs/content/dns/zz_gen_myaddr.md @@ -27,7 +27,7 @@ Here is an example bash command using the myaddr.{tools,dev,io} provider: ```bash MYADDR_PRIVATE_KEYS_MAPPING="example:123,test:456" \ -lego --email you@example.com --dns myaddr -d '*.example.com' -d example.com run +lego --dns myaddr -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_mydnsjp.md b/docs/content/dns/zz_gen_mydnsjp.md index 5b29266db..0a49404bb 100644 --- a/docs/content/dns/zz_gen_mydnsjp.md +++ b/docs/content/dns/zz_gen_mydnsjp.md @@ -28,7 +28,7 @@ Here is an example bash command using the MyDNS.jp provider: ```bash MYDNSJP_MASTER_ID=xxxxx \ MYDNSJP_PASSWORD=xxxxx \ -lego --email you@example.com --dns mydnsjp -d '*.example.com' -d example.com run +lego --dns mydnsjp -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_mythicbeasts.md b/docs/content/dns/zz_gen_mythicbeasts.md index 37feebf8c..70e38d249 100644 --- a/docs/content/dns/zz_gen_mythicbeasts.md +++ b/docs/content/dns/zz_gen_mythicbeasts.md @@ -28,7 +28,7 @@ Here is an example bash command using the MythicBeasts provider: ```bash MYTHICBEASTS_USERNAME=myuser \ MYTHICBEASTS_PASSWORD=mypass \ -lego --email you@example.com --dns mythicbeasts -d '*.example.com' -d example.com run +lego --dns mythicbeasts -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_namecheap.md b/docs/content/dns/zz_gen_namecheap.md index 706651660..9d7143d84 100644 --- a/docs/content/dns/zz_gen_namecheap.md +++ b/docs/content/dns/zz_gen_namecheap.md @@ -33,7 +33,7 @@ Here is an example bash command using the Namecheap provider: ```bash NAMECHEAP_API_USER=user \ NAMECHEAP_API_KEY=key \ -lego --email you@example.com --dns namecheap -d '*.example.com' -d example.com run +lego --dns namecheap -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_namedotcom.md b/docs/content/dns/zz_gen_namedotcom.md index 36a423faa..2860ff0ae 100644 --- a/docs/content/dns/zz_gen_namedotcom.md +++ b/docs/content/dns/zz_gen_namedotcom.md @@ -28,7 +28,7 @@ Here is an example bash command using the Name.com provider: ```bash NAMECOM_USERNAME=foo.bar \ NAMECOM_API_TOKEN=a379a6f6eeafb9a55e378c118034e2751e682fab \ -lego --email you@example.com --dns namedotcom -d '*.example.com' -d example.com run +lego --dns namedotcom -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_namesilo.md b/docs/content/dns/zz_gen_namesilo.md index 397a1a3ca..207a1603f 100644 --- a/docs/content/dns/zz_gen_namesilo.md +++ b/docs/content/dns/zz_gen_namesilo.md @@ -27,7 +27,7 @@ Here is an example bash command using the Namesilo provider: ```bash NAMESILO_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ -lego --email you@example.com --dns namesilo -d '*.example.com' -d example.com run +lego --dns namesilo -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_namesurfer.md b/docs/content/dns/zz_gen_namesurfer.md new file mode 100644 index 000000000..9a2802d0e --- /dev/null +++ b/docs/content/dns/zz_gen_namesurfer.md @@ -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/" +--- + + + + + + +Configuration for [FusionLayer NameSurfer](https://www.fusionlayer.com/). + + + + +- 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) + + + + diff --git a/docs/content/dns/zz_gen_nearlyfreespeech.md b/docs/content/dns/zz_gen_nearlyfreespeech.md index 86f6152f9..31402d2d2 100644 --- a/docs/content/dns/zz_gen_nearlyfreespeech.md +++ b/docs/content/dns/zz_gen_nearlyfreespeech.md @@ -28,7 +28,7 @@ Here is an example bash command using the NearlyFreeSpeech.NET provider: ```bash NEARLYFREESPEECH_API_KEY=xxxxxx \ NEARLYFREESPEECH_LOGIN=xxxx \ -lego --email you@example.com --dns nearlyfreespeech -d '*.example.com' -d example.com run +lego --dns nearlyfreespeech -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_neodigit.md b/docs/content/dns/zz_gen_neodigit.md index 70dfb6343..aefeef4bf 100644 --- a/docs/content/dns/zz_gen_neodigit.md +++ b/docs/content/dns/zz_gen_neodigit.md @@ -27,7 +27,7 @@ Here is an example bash command using the Neodigit provider: ```bash NEODIGIT_TOKEN=xxxxxx \ -lego --email you@example.com --dns neodigit -d '*.example.com' -d example.com run +lego --dns neodigit -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_netcup.md b/docs/content/dns/zz_gen_netcup.md index 337baf59d..29def3285 100644 --- a/docs/content/dns/zz_gen_netcup.md +++ b/docs/content/dns/zz_gen_netcup.md @@ -29,7 +29,7 @@ Here is an example bash command using the Netcup provider: NETCUP_CUSTOMER_NUMBER=xxxx \ NETCUP_API_KEY=yyyy \ NETCUP_API_PASSWORD=zzzz \ -lego --email you@example.com --dns netcup -d '*.example.com' -d example.com run +lego --dns netcup -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_netlify.md b/docs/content/dns/zz_gen_netlify.md index b08f650f0..76651d9ef 100644 --- a/docs/content/dns/zz_gen_netlify.md +++ b/docs/content/dns/zz_gen_netlify.md @@ -27,7 +27,7 @@ Here is an example bash command using the Netlify provider: ```bash NETLIFY_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns netlify -d '*.example.com' -d example.com run +lego --dns netlify -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_nicmanager.md b/docs/content/dns/zz_gen_nicmanager.md index 0b6e1b2cb..a29d72120 100644 --- a/docs/content/dns/zz_gen_nicmanager.md +++ b/docs/content/dns/zz_gen_nicmanager.md @@ -34,7 +34,7 @@ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ -lego --email you@example.com --dns nicmanager -d '*.example.com' -d example.com run +lego --dns nicmanager -d '*.example.com' -d example.com run ## Login using account name + username @@ -45,7 +45,7 @@ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ -lego --email you@example.com --dns nicmanager -d '*.example.com' -d example.com run +lego --dns nicmanager -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_nicru.md b/docs/content/dns/zz_gen_nicru.md index d55477a32..3ac8d99cf 100644 --- a/docs/content/dns/zz_gen_nicru.md +++ b/docs/content/dns/zz_gen_nicru.md @@ -30,7 +30,7 @@ NICRU_USER="" \ NICRU_PASSWORD="" \ NICRU_SERVICE_ID="" \ NICRU_SECRET="" \ -lego --dns nicru --domains "*.example.com" --email you@example.com run +lego --dns nicru -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_nifcloud.md b/docs/content/dns/zz_gen_nifcloud.md index 9b9929ce2..66f38223b 100644 --- a/docs/content/dns/zz_gen_nifcloud.md +++ b/docs/content/dns/zz_gen_nifcloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the NIFCloud provider: ```bash NIFCLOUD_ACCESS_KEY_ID=xxxx \ NIFCLOUD_SECRET_ACCESS_KEY=yyyy \ -lego --email you@example.com --dns nifcloud -d '*.example.com' -d example.com run +lego --dns nifcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_njalla.md b/docs/content/dns/zz_gen_njalla.md index cf268041c..9a312df8b 100644 --- a/docs/content/dns/zz_gen_njalla.md +++ b/docs/content/dns/zz_gen_njalla.md @@ -27,7 +27,7 @@ Here is an example bash command using the Njalla provider: ```bash NJALLA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns njalla -d '*.example.com' -d example.com run +lego --dns njalla -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_nodion.md b/docs/content/dns/zz_gen_nodion.md index c11759e8e..8d61eb834 100644 --- a/docs/content/dns/zz_gen_nodion.md +++ b/docs/content/dns/zz_gen_nodion.md @@ -27,7 +27,7 @@ Here is an example bash command using the Nodion provider: ```bash NODION_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns nodion -d '*.example.com' -d example.com run +lego --dns nodion -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ns1.md b/docs/content/dns/zz_gen_ns1.md index 547a51c1c..b2262169d 100644 --- a/docs/content/dns/zz_gen_ns1.md +++ b/docs/content/dns/zz_gen_ns1.md @@ -27,7 +27,7 @@ Here is an example bash command using the NS1 provider: ```bash NS1_API_KEY=xxxx \ -lego --email you@example.com --dns ns1 -d '*.example.com' -d example.com run +lego --dns ns1 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_octenium.md b/docs/content/dns/zz_gen_octenium.md index 874c4e780..f25da4f44 100644 --- a/docs/content/dns/zz_gen_octenium.md +++ b/docs/content/dns/zz_gen_octenium.md @@ -27,7 +27,7 @@ Here is an example bash command using the Octenium provider: ```bash OCTENIUM_API_KEY="xxx" \ -lego --email you@example.com --dns octenium -d '*.example.com' -d example.com run +lego --dns octenium -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_oraclecloud.md b/docs/content/dns/zz_gen_oraclecloud.md index c43c24b21..b7192f380 100644 --- a/docs/content/dns/zz_gen_oraclecloud.md +++ b/docs/content/dns/zz_gen_oraclecloud.md @@ -34,13 +34,13 @@ OCI_USER_OCID="ocid1.user.oc1..secret" \ OCI_FINGERPRINT="00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" \ OCI_REGION="us-phoenix-1" \ OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ -lego --email you@example.com --dns oraclecloud -d '*.example.com' -d example.com run +lego --dns oraclecloud -d '*.example.com' -d example.com run # Using Instance Principal authentication (when running on OCI compute instances): # https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm OCI_AUTH_TYPE="instance_principal" \ OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ -lego --email you@example.com --dns oraclecloud -d '*.example.com' -d example.com run +lego --dns oraclecloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_otc.md b/docs/content/dns/zz_gen_otc.md index 4f3679fa2..9da69c694 100644 --- a/docs/content/dns/zz_gen_otc.md +++ b/docs/content/dns/zz_gen_otc.md @@ -30,7 +30,7 @@ OTC_DOMAIN_NAME=domain_name \ OTC_USER_NAME=user_name \ OTC_PASSWORD=password \ OTC_PROJECT_NAME=project_name \ -lego --email you@example.com --dns otc -d '*.example.com' -d example.com run +lego --dns otc -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ovh.md b/docs/content/dns/zz_gen_ovh.md index 7abc01b92..aaafded85 100644 --- a/docs/content/dns/zz_gen_ovh.md +++ b/docs/content/dns/zz_gen_ovh.md @@ -32,20 +32,20 @@ OVH_APPLICATION_KEY=1234567898765432 \ OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \ OVH_CONSUMER_KEY=256vfsd347245sdfg \ OVH_ENDPOINT=ovh-eu \ -lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run +lego --dns ovh -d '*.example.com' -d example.com run # Or Access Token: OVH_ACCESS_TOKEN=xxx \ OVH_ENDPOINT=ovh-eu \ -lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run +lego --dns ovh -d '*.example.com' -d example.com run # Or OAuth2: OVH_CLIENT_ID=yyy \ OVH_CLIENT_SECRET=xxx \ OVH_ENDPOINT=ovh-eu \ -lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run +lego --dns ovh -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_pdns.md b/docs/content/dns/zz_gen_pdns.md index 34a22cf84..7c2a8c663 100644 --- a/docs/content/dns/zz_gen_pdns.md +++ b/docs/content/dns/zz_gen_pdns.md @@ -28,7 +28,7 @@ Here is an example bash command using the PowerDNS provider: ```bash PDNS_API_URL=http://pdns-server:80/ \ PDNS_API_KEY=xxxx \ -lego --email you@example.com --dns pdns -d '*.example.com' -d example.com run +lego --dns pdns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_plesk.md b/docs/content/dns/zz_gen_plesk.md index b18b2656a..73ec9a55d 100644 --- a/docs/content/dns/zz_gen_plesk.md +++ b/docs/content/dns/zz_gen_plesk.md @@ -29,7 +29,7 @@ Here is an example bash command using the plesk.com provider: PLESK_SERVER_BASE_URL="https://plesk.myserver.com:8443" \ PLESK_USERNAME=xxxxxx \ PLESK_PASSWORD=yyyyyy \ -lego --email you@example.com --dns plesk -d '*.example.com' -d example.com run +lego --dns plesk -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_porkbun.md b/docs/content/dns/zz_gen_porkbun.md index 9fd230d0d..f54e6f688 100644 --- a/docs/content/dns/zz_gen_porkbun.md +++ b/docs/content/dns/zz_gen_porkbun.md @@ -28,7 +28,7 @@ Here is an example bash command using the Porkbun provider: ```bash PORKBUN_SECRET_API_KEY=xxxxxx \ PORKBUN_API_KEY=yyyyyy \ -lego --email you@example.com --dns porkbun -d '*.example.com' -d example.com run +lego --dns porkbun -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_rackspace.md b/docs/content/dns/zz_gen_rackspace.md index 6dcf6b2b2..b9a2ab710 100644 --- a/docs/content/dns/zz_gen_rackspace.md +++ b/docs/content/dns/zz_gen_rackspace.md @@ -28,7 +28,7 @@ Here is an example bash command using the Rackspace provider: ```bash RACKSPACE_USER=xxxx \ RACKSPACE_API_KEY=yyyy \ -lego --email you@example.com --dns rackspace -d '*.example.com' -d example.com run +lego --dns rackspace -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_rainyun.md b/docs/content/dns/zz_gen_rainyun.md index 74ced9f54..680eb845a 100644 --- a/docs/content/dns/zz_gen_rainyun.md +++ b/docs/content/dns/zz_gen_rainyun.md @@ -27,7 +27,7 @@ Here is an example bash command using the Rain Yun/雨云 provider: ```bash RAINYUN_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns rainyun -d '*.example.com' -d example.com run +lego --dns rainyun -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_rcodezero.md b/docs/content/dns/zz_gen_rcodezero.md index 98eaea9ca..a544df420 100644 --- a/docs/content/dns/zz_gen_rcodezero.md +++ b/docs/content/dns/zz_gen_rcodezero.md @@ -27,7 +27,7 @@ Here is an example bash command using the RcodeZero provider: ```bash RCODEZERO_API_TOKEN= \ -lego --email you@example.com --dns rcodezero -d '*.example.com' -d example.com run +lego --dns rcodezero -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_regfish.md b/docs/content/dns/zz_gen_regfish.md index 149338e5e..357ce0764 100644 --- a/docs/content/dns/zz_gen_regfish.md +++ b/docs/content/dns/zz_gen_regfish.md @@ -27,7 +27,7 @@ Here is an example bash command using the Regfish provider: ```bash REGFISH_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns regfish -d '*.example.com' -d example.com run +lego --dns regfish -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_regru.md b/docs/content/dns/zz_gen_regru.md index 1d0e0053d..eaf163a13 100644 --- a/docs/content/dns/zz_gen_regru.md +++ b/docs/content/dns/zz_gen_regru.md @@ -28,7 +28,7 @@ Here is an example bash command using the reg.ru provider: ```bash REGRU_USERNAME=xxxxxx \ REGRU_PASSWORD=yyyyyy \ -lego --email you@example.com --dns regru -d '*.example.com' -d example.com run +lego --dns regru -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_rfc2136.md b/docs/content/dns/zz_gen_rfc2136.md index ffdbc4b54..1b1d43dd5 100644 --- a/docs/content/dns/zz_gen_rfc2136.md +++ b/docs/content/dns/zz_gen_rfc2136.md @@ -30,7 +30,7 @@ RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_KEY=example.com \ RFC2136_TSIG_ALGORITHM=hmac-sha256. \ RFC2136_TSIG_SECRET=YWJjZGVmZGdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNDU= \ -lego --email you@example.com --dns rfc2136 -d '*.example.com' -d example.com run +lego --dns rfc2136 -d '*.example.com' -d example.com run ## --- @@ -38,7 +38,7 @@ keyname=example.com; keyfile=example.com.key; tsig-keygen $keyname > $keyfile RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_FILE="$keyfile" \ -lego --email you@example.com --dns rfc2136 -d '*.example.com' -d example.com run +lego --dns rfc2136 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_rimuhosting.md b/docs/content/dns/zz_gen_rimuhosting.md index 2a703dec7..acb829e93 100644 --- a/docs/content/dns/zz_gen_rimuhosting.md +++ b/docs/content/dns/zz_gen_rimuhosting.md @@ -27,7 +27,7 @@ Here is an example bash command using the RimuHosting provider: ```bash RIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns rimuhosting -d '*.example.com' -d example.com run +lego --dns rimuhosting -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_route53.md b/docs/content/dns/zz_gen_route53.md index a0967a57e..59e489d6a 100644 --- a/docs/content/dns/zz_gen_route53.md +++ b/docs/content/dns/zz_gen_route53.md @@ -30,7 +30,7 @@ AWS_ACCESS_KEY_ID=your_key_id \ AWS_SECRET_ACCESS_KEY=your_secret_access_key \ AWS_REGION=aws-region \ AWS_HOSTED_ZONE_ID=your_hosted_zone_id \ -lego --email you@example.com --dns route53 -d '*.example.com' -d example.com run +lego --dns route53 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_safedns.md b/docs/content/dns/zz_gen_safedns.md index 2a9e179f5..4c20fca6a 100644 --- a/docs/content/dns/zz_gen_safedns.md +++ b/docs/content/dns/zz_gen_safedns.md @@ -1,12 +1,12 @@ --- -title: "UKFast SafeDNS" +title: "ANS SafeDNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: safedns dnsprovider: since: "v4.6.0" code: "safedns" - url: "https://www.ukfast.co.uk/dns-hosting.html" + url: "https://www.ans.co.uk/" --- @@ -14,7 +14,7 @@ dnsprovider: -Configuration for [UKFast SafeDNS](https://www.ukfast.co.uk/dns-hosting.html). +Configuration for [ANS SafeDNS](https://www.ans.co.uk/). @@ -23,11 +23,11 @@ Configuration for [UKFast SafeDNS](https://www.ukfast.co.uk/dns-hosting.html). - Since: v4.6.0 -Here is an example bash command using the UKFast SafeDNS provider: +Here is an example bash command using the ANS SafeDNS provider: ```bash SAFEDNS_AUTH_TOKEN=xxxxxx \ -lego --email you@example.com --dns safedns -d '*.example.com' -d example.com run +lego --dns safedns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_sakuracloud.md b/docs/content/dns/zz_gen_sakuracloud.md index e08e73e70..b43f83ef4 100644 --- a/docs/content/dns/zz_gen_sakuracloud.md +++ b/docs/content/dns/zz_gen_sakuracloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the Sakura Cloud provider: ```bash SAKURACLOUD_ACCESS_TOKEN=xxxxx \ SAKURACLOUD_ACCESS_TOKEN_SECRET=yyyyy \ -lego --email you@example.com --dns sakuracloud -d '*.example.com' -d example.com run +lego --dns sakuracloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_scaleway.md b/docs/content/dns/zz_gen_scaleway.md index 2f6af9d8a..4033a9bd6 100644 --- a/docs/content/dns/zz_gen_scaleway.md +++ b/docs/content/dns/zz_gen_scaleway.md @@ -27,7 +27,7 @@ Here is an example bash command using the Scaleway provider: ```bash SCW_SECRET_KEY=xxxxxxx-xxxxx-xxxx-xxx-xxxxxx \ -lego --email you@example.com --dns scaleway -d '*.example.com' -d example.com run +lego --dns scaleway -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_selectel.md b/docs/content/dns/zz_gen_selectel.md index 33dc859bb..d994d6633 100644 --- a/docs/content/dns/zz_gen_selectel.md +++ b/docs/content/dns/zz_gen_selectel.md @@ -27,7 +27,7 @@ Here is an example bash command using the Selectel provider: ```bash SELECTEL_API_TOKEN=xxxxx \ -lego --email you@example.com --dns selectel -d '*.example.com' -d example.com run +lego --dns selectel -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_selectelv2.md b/docs/content/dns/zz_gen_selectelv2.md index 933ca201f..0873d810c 100644 --- a/docs/content/dns/zz_gen_selectelv2.md +++ b/docs/content/dns/zz_gen_selectelv2.md @@ -30,7 +30,7 @@ SELECTELV2_USERNAME=trex \ SELECTELV2_PASSWORD=xxxxx \ SELECTELV2_ACCOUNT_ID=1234567 \ SELECTELV2_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \ -lego --email you@example.com --dns selectelv2 -d '*.example.com' -d example.com run +lego --dns selectelv2 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_selfhostde.md b/docs/content/dns/zz_gen_selfhostde.md index 12df0c10d..363f782e0 100644 --- a/docs/content/dns/zz_gen_selfhostde.md +++ b/docs/content/dns/zz_gen_selfhostde.md @@ -29,7 +29,7 @@ Here is an example bash command using the SelfHost.(de|eu) provider: SELFHOSTDE_USERNAME=xxx \ SELFHOSTDE_PASSWORD=yyy \ SELFHOSTDE_RECORDS_MAPPING=my.example.com:123 \ -lego --email you@example.com --dns selfhostde -d '*.example.com' -d example.com run +lego --dns selfhostde -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_servercow.md b/docs/content/dns/zz_gen_servercow.md index 3851325d1..7d00a6306 100644 --- a/docs/content/dns/zz_gen_servercow.md +++ b/docs/content/dns/zz_gen_servercow.md @@ -28,7 +28,7 @@ Here is an example bash command using the Servercow provider: ```bash SERVERCOW_USERNAME=xxxxxxxx \ SERVERCOW_PASSWORD=xxxxxxxx \ -lego --email you@example.com --dns servercow -d '*.example.com' -d example.com run +lego --dns servercow -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_shellrent.md b/docs/content/dns/zz_gen_shellrent.md index 6c1365b7e..cbbc172e2 100644 --- a/docs/content/dns/zz_gen_shellrent.md +++ b/docs/content/dns/zz_gen_shellrent.md @@ -28,7 +28,7 @@ Here is an example bash command using the Shellrent provider: ```bash SHELLRENT_USERNAME=xxxx \ SHELLRENT_TOKEN=yyyy \ -lego --email you@example.com --dns shellrent -d '*.example.com' -d example.com run +lego --dns shellrent -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_simply.md b/docs/content/dns/zz_gen_simply.md index 32df66f05..edfa14380 100644 --- a/docs/content/dns/zz_gen_simply.md +++ b/docs/content/dns/zz_gen_simply.md @@ -28,7 +28,7 @@ Here is an example bash command using the Simply.com provider: ```bash SIMPLY_ACCOUNT_NAME=xxxxxx \ SIMPLY_API_KEY=yyyyyy \ -lego --email you@example.com --dns simply -d '*.example.com' -d example.com run +lego --dns simply -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_sonic.md b/docs/content/dns/zz_gen_sonic.md index f56a23151..20729bc1a 100644 --- a/docs/content/dns/zz_gen_sonic.md +++ b/docs/content/dns/zz_gen_sonic.md @@ -28,7 +28,7 @@ Here is an example bash command using the Sonic provider: ```bash SONIC_USER_ID=12345 \ SONIC_API_KEY=4d6fbf2f9ab0fa11697470918d37625851fc0c51 \ -lego --email you@example.com --dns sonic -d '*.example.com' -d example.com run +lego --dns sonic -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_spaceship.md b/docs/content/dns/zz_gen_spaceship.md index 4594fe217..9f3b51e43 100644 --- a/docs/content/dns/zz_gen_spaceship.md +++ b/docs/content/dns/zz_gen_spaceship.md @@ -28,7 +28,7 @@ Here is an example bash command using the Spaceship provider: ```bash SPACESHIP_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ SPACESHIP_API_SECRET="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns spaceship -d '*.example.com' -d example.com run +lego --dns spaceship -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_stackpath.md b/docs/content/dns/zz_gen_stackpath.md index ce0a02eac..b881176f4 100644 --- a/docs/content/dns/zz_gen_stackpath.md +++ b/docs/content/dns/zz_gen_stackpath.md @@ -29,7 +29,7 @@ Here is an example bash command using the Stackpath provider: STACKPATH_CLIENT_ID=xxxxx \ STACKPATH_CLIENT_SECRET=yyyyy \ STACKPATH_STACK_ID=zzzzz \ -lego --email you@example.com --dns stackpath -d '*.example.com' -d example.com run +lego --dns stackpath -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_syse.md b/docs/content/dns/zz_gen_syse.md index 1d9d957d5..a1a952bc5 100644 --- a/docs/content/dns/zz_gen_syse.md +++ b/docs/content/dns/zz_gen_syse.md @@ -27,10 +27,10 @@ Here is an example bash command using the Syse provider: ```bash SYSE_CREDENTIALS=example.com:password \ -lego --email you@example.com --dns syse -d '*.example.com' -d example.com run +lego --dns syse -d '*.example.com' -d example.com run SYSE_CREDENTIALS=example.org:password1,example.com:password2 \ -lego --email you@example.com --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com +lego --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com ``` diff --git a/docs/content/dns/zz_gen_technitium.md b/docs/content/dns/zz_gen_technitium.md index 80f7c6a1f..ff7f2e6ed 100644 --- a/docs/content/dns/zz_gen_technitium.md +++ b/docs/content/dns/zz_gen_technitium.md @@ -28,7 +28,7 @@ Here is an example bash command using the Technitium provider: ```bash TECHNITIUM_SERVER_BASE_URL="https://localhost:5380" \ TECHNITIUM_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns technitium -d '*.example.com' -d example.com run +lego --dns technitium -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_tencentcloud.md b/docs/content/dns/zz_gen_tencentcloud.md index ef1e6cdf8..178ffcf43 100644 --- a/docs/content/dns/zz_gen_tencentcloud.md +++ b/docs/content/dns/zz_gen_tencentcloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the Tencent Cloud DNS provider: ```bash TENCENTCLOUD_SECRET_ID=abcdefghijklmnopqrstuvwx \ TENCENTCLOUD_SECRET_KEY=your-secret-key \ -lego --email you@example.com --dns tencentcloud -d '*.example.com' -d example.com run +lego --dns tencentcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_timewebcloud.md b/docs/content/dns/zz_gen_timewebcloud.md index af218ddce..83d5b831b 100644 --- a/docs/content/dns/zz_gen_timewebcloud.md +++ b/docs/content/dns/zz_gen_timewebcloud.md @@ -27,7 +27,7 @@ Here is an example bash command using the Timeweb Cloud provider: ```bash TIMEWEBCLOUD_AUTH_TOKEN=xxxxxx \ -lego --email you@example.com --dns timewebcloud -d '*.example.com' -d example.com run +lego --dns timewebcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_todaynic.md b/docs/content/dns/zz_gen_todaynic.md new file mode 100644 index 000000000..7b06c012d --- /dev/null +++ b/docs/content/dns/zz_gen_todaynic.md @@ -0,0 +1,69 @@ +--- +title: "TodayNIC/时代互联" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: todaynic +dnsprovider: + since: "v4.32.0" + code: "todaynic" + url: "https://www.todaynic.com/" +--- + + + + + + +Configuration for [TodayNIC/时代互联](https://www.todaynic.com/). + + + + +- Code: `todaynic` +- Since: v4.32.0 + + +Here is an example bash command using the TodayNIC/时代互联 provider: + +```bash +TODAYNIC_AUTH_USER_ID="xxx" \ +TODAYNIC_API_KEY="yyy" \ +lego --dns todaynic -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `TODAYNIC_API_KEY` | API key | +| `TODAYNIC_AUTH_USER_ID` | account ID | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `TODAYNIC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `TODAYNIC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `TODAYNIC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `TODAYNIC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + + + +## More information + +- [API documentation](https://www.todaynic.com/partner/mode_Http_Api_detail.php) + + + + diff --git a/docs/content/dns/zz_gen_transip.md b/docs/content/dns/zz_gen_transip.md index 769fbc734..a66a25879 100644 --- a/docs/content/dns/zz_gen_transip.md +++ b/docs/content/dns/zz_gen_transip.md @@ -28,7 +28,7 @@ Here is an example bash command using the TransIP provider: ```bash TRANSIP_ACCOUNT_NAME = "Account name" \ TRANSIP_PRIVATE_KEY_PATH = "transip.key" \ -lego --email you@example.com --dns transip -d '*.example.com' -d example.com run +lego --dns transip -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ultradns.md b/docs/content/dns/zz_gen_ultradns.md index 8e0fa9b20..d6d89c77b 100644 --- a/docs/content/dns/zz_gen_ultradns.md +++ b/docs/content/dns/zz_gen_ultradns.md @@ -28,7 +28,7 @@ Here is an example bash command using the Ultradns provider: ```bash ULTRADNS_USERNAME=username \ ULTRADNS_PASSWORD=password \ -lego --email you@example.com --dns ultradns -d '*.example.com' -d example.com run +lego --dns ultradns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_uniteddomains.md b/docs/content/dns/zz_gen_uniteddomains.md index 7f94dd09f..e837644d5 100644 --- a/docs/content/dns/zz_gen_uniteddomains.md +++ b/docs/content/dns/zz_gen_uniteddomains.md @@ -27,7 +27,7 @@ Here is an example bash command using the United-Domains provider: ```bash UNITEDDOMAINS_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns uniteddomains -d '*.example.com' -d example.com run +lego --dns uniteddomains -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_variomedia.md b/docs/content/dns/zz_gen_variomedia.md index 282ec9da3..f9771c867 100644 --- a/docs/content/dns/zz_gen_variomedia.md +++ b/docs/content/dns/zz_gen_variomedia.md @@ -27,7 +27,7 @@ Here is an example bash command using the Variomedia provider: ```bash VARIOMEDIA_API_TOKEN=xxxx \ -lego --email you@example.com --dns variomedia -d '*.example.com' -d example.com run +lego --dns variomedia -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_vercel.md b/docs/content/dns/zz_gen_vercel.md index d9e24eee3..71f2eeed5 100644 --- a/docs/content/dns/zz_gen_vercel.md +++ b/docs/content/dns/zz_gen_vercel.md @@ -27,7 +27,7 @@ Here is an example bash command using the Vercel provider: ```bash VERCEL_API_TOKEN=xxxxxx \ -lego --email you@example.com --dns vercel -d '*.example.com' -d example.com run +lego --dns vercel -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_versio.md b/docs/content/dns/zz_gen_versio.md index 0e2edfa1e..5d2cc0118 100644 --- a/docs/content/dns/zz_gen_versio.md +++ b/docs/content/dns/zz_gen_versio.md @@ -28,7 +28,7 @@ Here is an example bash command using the Versio.[nl|eu|uk] provider: ```bash VERSIO_USERNAME= \ VERSIO_PASSWORD= \ -lego --email you@example.com --dns versio -d '*.example.com' -d example.com run +lego --dns versio -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_vinyldns.md b/docs/content/dns/zz_gen_vinyldns.md index 666bc39c4..3280d6f0a 100644 --- a/docs/content/dns/zz_gen_vinyldns.md +++ b/docs/content/dns/zz_gen_vinyldns.md @@ -29,7 +29,7 @@ Here is an example bash command using the VinylDNS provider: VINYLDNS_ACCESS_KEY=xxxxxx \ VINYLDNS_SECRET_KEY=yyyyy \ VINYLDNS_HOST=https://api.vinyldns.example.org:9443 \ -lego --email you@example.com --dns vinyldns -d '*.example.com' -d example.com run +lego --dns vinyldns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_virtualname.md b/docs/content/dns/zz_gen_virtualname.md index afba24ad0..a00e5105f 100644 --- a/docs/content/dns/zz_gen_virtualname.md +++ b/docs/content/dns/zz_gen_virtualname.md @@ -27,7 +27,7 @@ Here is an example bash command using the Virtualname provider: ```bash VIRTUALNAME_TOKEN=xxxxxx \ -lego --email you@example.com --dns virtualname -d '*.example.com' -d example.com run +lego --dns virtualname -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_vkcloud.md b/docs/content/dns/zz_gen_vkcloud.md index eede62cf5..76fd557a5 100644 --- a/docs/content/dns/zz_gen_vkcloud.md +++ b/docs/content/dns/zz_gen_vkcloud.md @@ -29,7 +29,7 @@ Here is an example bash command using the VK Cloud provider: VK_CLOUD_PROJECT_ID="" \ VK_CLOUD_USERNAME="" \ VK_CLOUD_PASSWORD="" \ -lego --email you@example.com --dns vkcloud -d '*.example.com' -d example.com run +lego --dns vkcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_volcengine.md b/docs/content/dns/zz_gen_volcengine.md index 9d3c92d0d..587ce1e74 100644 --- a/docs/content/dns/zz_gen_volcengine.md +++ b/docs/content/dns/zz_gen_volcengine.md @@ -28,7 +28,7 @@ Here is an example bash command using the Volcano Engine/火山引擎 provider: ```bash VOLC_ACCESSKEY=xxx \ VOLC_SECRETKEY=yyy \ -lego --email you@example.com --dns volcengine -d '*.example.com' -d example.com run +lego --dns volcengine -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_vscale.md b/docs/content/dns/zz_gen_vscale.md index 660542d61..c33e2f7b5 100644 --- a/docs/content/dns/zz_gen_vscale.md +++ b/docs/content/dns/zz_gen_vscale.md @@ -27,7 +27,7 @@ Here is an example bash command using the Vscale provider: ```bash VSCALE_API_TOKEN=xxxxx \ -lego --email you@example.com --dns vscale -d '*.example.com' -d example.com run +lego --dns vscale -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_vultr.md b/docs/content/dns/zz_gen_vultr.md index a3807c1a1..4160fbcf3 100644 --- a/docs/content/dns/zz_gen_vultr.md +++ b/docs/content/dns/zz_gen_vultr.md @@ -27,7 +27,7 @@ Here is an example bash command using the Vultr provider: ```bash VULTR_API_KEY=xxxxx \ -lego --email you@example.com --dns vultr -d '*.example.com' -d example.com run +lego --dns vultr -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_webnames.md b/docs/content/dns/zz_gen_webnames.md index 4945775a5..cad02c287 100644 --- a/docs/content/dns/zz_gen_webnames.md +++ b/docs/content/dns/zz_gen_webnames.md @@ -27,7 +27,7 @@ Here is an example bash command using the webnames.ru provider: ```bash WEBNAMESRU_API_KEY=xxxxxx \ -lego --email you@example.com --dns webnamesru -d '*.example.com' -d example.com run +lego --dns webnamesru -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_webnamesca.md b/docs/content/dns/zz_gen_webnamesca.md index 41a33cb82..4a7d3794f 100644 --- a/docs/content/dns/zz_gen_webnamesca.md +++ b/docs/content/dns/zz_gen_webnamesca.md @@ -28,7 +28,7 @@ Here is an example bash command using the webnames.ca provider: ```bash WEBNAMESCA_API_USER="xxx" \ WEBNAMESCA_API_KEY="yyy" \ -lego --email you@example.com --dns webnamesca -d '*.example.com' -d example.com run +lego --dns webnamesca -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_websupport.md b/docs/content/dns/zz_gen_websupport.md index 5fe44a860..67ae394d7 100644 --- a/docs/content/dns/zz_gen_websupport.md +++ b/docs/content/dns/zz_gen_websupport.md @@ -28,7 +28,7 @@ Here is an example bash command using the Websupport provider: ```bash WEBSUPPORT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ WEBSUPPORT_SECRET="yyyyyyyyyyyyyyyyyyyyy" \ -lego --email you@example.com --dns websupport -d '*.example.com' -d example.com run +lego --dns websupport -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_wedos.md b/docs/content/dns/zz_gen_wedos.md index 8fe6ba00d..16139f4d4 100644 --- a/docs/content/dns/zz_gen_wedos.md +++ b/docs/content/dns/zz_gen_wedos.md @@ -28,7 +28,7 @@ Here is an example bash command using the WEDOS provider: ```bash WEDOS_USERNAME=xxxxxxxx \ WEDOS_WAPI_PASSWORD=xxxxxxxx \ -lego --email you@example.com --dns wedos -d '*.example.com' -d example.com run +lego --dns wedos -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_westcn.md b/docs/content/dns/zz_gen_westcn.md index 434e5b601..a5523b955 100644 --- a/docs/content/dns/zz_gen_westcn.md +++ b/docs/content/dns/zz_gen_westcn.md @@ -28,7 +28,7 @@ Here is an example bash command using the West.cn/西部数码 provider: ```bash WESTCN_USERNAME="xxx" \ WESTCN_PASSWORD="yyy" \ -lego --email you@example.com --dns westcn -d '*.example.com' -d example.com run +lego --dns westcn -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_yandex.md b/docs/content/dns/zz_gen_yandex.md index 6100c02fe..4a1cf1f99 100644 --- a/docs/content/dns/zz_gen_yandex.md +++ b/docs/content/dns/zz_gen_yandex.md @@ -27,7 +27,7 @@ Here is an example bash command using the Yandex PDD provider: ```bash YANDEX_PDD_TOKEN= \ -lego --email you@example.com --dns yandex -d '*.example.com' -d example.com run +lego --dns yandex -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_yandex360.md b/docs/content/dns/zz_gen_yandex360.md index 66b90e049..d831fdfc2 100644 --- a/docs/content/dns/zz_gen_yandex360.md +++ b/docs/content/dns/zz_gen_yandex360.md @@ -28,7 +28,7 @@ Here is an example bash command using the Yandex 360 provider: ```bash YANDEX360_OAUTH_TOKEN= \ YANDEX360_ORG_ID= \ -lego --email you@example.com --dns yandex360 -d '*.example.com' -d example.com run +lego --dns yandex360 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_yandexcloud.md b/docs/content/dns/zz_gen_yandexcloud.md index f5aeba09d..0564e93d2 100644 --- a/docs/content/dns/zz_gen_yandexcloud.md +++ b/docs/content/dns/zz_gen_yandexcloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the Yandex Cloud provider: ```bash YANDEX_CLOUD_IAM_TOKEN= \ YANDEX_CLOUD_FOLDER_ID= \ -lego --email you@example.com --dns yandexcloud -d '*.example.com' -d example.com run +lego --dns yandexcloud -d '*.example.com' -d example.com run # --- @@ -41,7 +41,7 @@ YANDEX_CLOUD_IAM_TOKEN=$(echo '{ \ "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----" \ }' | base64) \ YANDEX_CLOUD_FOLDER_ID= \ -lego --email you@example.com --dns yandexcloud -d '*.example.com' -d example.com run +lego --dns yandexcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_zoneedit.md b/docs/content/dns/zz_gen_zoneedit.md index e259a2a04..c7f88b3fe 100644 --- a/docs/content/dns/zz_gen_zoneedit.md +++ b/docs/content/dns/zz_gen_zoneedit.md @@ -28,7 +28,7 @@ Here is an example bash command using the ZoneEdit provider: ```bash ZONEEDIT_USER="xxxxxxxxxxxxxxxxxxxxx" \ ZONEEDIT_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns zoneedit -d '*.example.com' -d example.com run +lego --dns zoneedit -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_zoneee.md b/docs/content/dns/zz_gen_zoneee.md index cfc6be692..65678a3dc 100644 --- a/docs/content/dns/zz_gen_zoneee.md +++ b/docs/content/dns/zz_gen_zoneee.md @@ -28,7 +28,7 @@ Here is an example bash command using the Zone.ee provider: ```bash ZONEEE_API_USER=xxxxx \ ZONEEE_API_KEY=yyyyy \ -lego --email you@example.com --dns zoneee -d '*.example.com' -d example.com run +lego --dns zoneee -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_zonomi.md b/docs/content/dns/zz_gen_zonomi.md index 1e90a7285..fd8757f82 100644 --- a/docs/content/dns/zz_gen_zonomi.md +++ b/docs/content/dns/zz_gen_zonomi.md @@ -27,7 +27,7 @@ Here is an example bash command using the Zonomi provider: ```bash ZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns zonomi -d '*.example.com' -d example.com run +lego --dns zonomi -d '*.example.com' -d example.com run ``` diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index fdb13f57a..139143b17 100644 --- a/docs/data/zz_cli_help.toml +++ b/docs/data/zz_cli_help.toml @@ -152,7 +152,7 @@ To display the documentation for a specific DNS provider, run: $ lego dnshelp -c code Supported DNS providers: - acme-dns, active24, alidns, aliesa, allinkl, anexia, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, conohav3, constellix, corenetworks, cpanel, derak, desec, designate, digitalocean, directadmin, 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, iwantmyname, 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, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi + acme-dns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, eurodns, excedo, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, googledomains, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, iwantmyname, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, nearlyfreespeech, neodigit, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi More information: https://go-acme.github.io/lego/dns """ diff --git a/e2e/fixtures/certs/localhost/cert.pem b/e2e/fixtures/certs/localhost/cert.pem index 2866a2b48..d81d29e70 100644 --- a/e2e/fixtures/certs/localhost/cert.pem +++ b/e2e/fixtures/certs/localhost/cert.pem @@ -1,19 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDGzCCAgOgAwIBAgIIbEfayDFsBtwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE -AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMDcx -MjA2MTk0MjEwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQCbFMW3DXXdErvQf2lCZ0qz0DGEWadDoF0O2neM5mVa -VQ7QGW0xc5Qwvn3Tl62C0JtwLpF0pG2BICIN+DHdVaIUwkf77iBS2doH1I3waE1I -8GkV9JrYmFY+j0dA1SwBmqUZNXhLNwZGq1a91nFSI59DZNy/JciqxoPX2K++ojU2 -FPpuXe2t51NmXMsszpa+TDqF/IeskA9A/ws6UIh4Mzhghx7oay2/qqj2IIPjAmJj -i73kdUvtEry3wmlkBvtVH50+FscS9WmPC5h3lDTk5nbzSAXKuFusotuqy3XTgY5B -PiRAwkZbEY43JNfqenQPHo7mNTt29i+NVVrBsnAa5ovrAgMBAAGjYzBhMA4GA1Ud -DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T -AQH/BAIwADAiBgNVHREEGzAZgglsb2NhbGhvc3SCBnBlYmJsZYcEfwAAATANBgkq -hkiG9w0BAQsFAAOCAQEAYIkXff8H28KS0KyLHtbbSOGU4sujHHVwiVXSATACsNAE -D0Qa8hdtTQ6AUqA6/n8/u1tk0O4rPE/cTpsM3IJFX9S3rZMRsguBP7BSr1Lq/XAB -7JP/CNHt+Z9aKCKcg11wIX9/B9F7pyKM3TdKgOpqXGV6TMuLjg5PlYWI/07lVGFW -/mSJDRs8bSCFmbRtEqc4lpwlrpz+kTTnX6G7JDLfLWYw/xXVqwFfdengcDTHCc8K -wtgGq/Gu6vcoBxIO3jaca+OIkMfxxXmGrcNdseuUCa3RMZ8Qy03DqGu6Y6XQyK4B -W8zIG6H9SVKkAznM2yfYhW8v2ktcaZ95/OBHY97ZIw== +MIIDMDCCAhigAwIBAgIILDt8c2fMw2IwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgNTM0NWU2MB4XDTI1MDkwMzIzNDAwNVoXDTI3MTAw +MzIzNDAwNVowFDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAmxTFtw113RK70H9pQmdKs9AxhFmnQ6BdDtp3jOZlWlUO +0BltMXOUML5905etgtCbcC6RdKRtgSAiDfgx3VWiFMJH++4gUtnaB9SN8GhNSPBp +FfSa2JhWPo9HQNUsAZqlGTV4SzcGRqtWvdZxUiOfQ2TcvyXIqsaD19ivvqI1NhT6 +bl3tredTZlzLLM6Wvkw6hfyHrJAPQP8LOlCIeDM4YIce6Gstv6qo9iCD4wJiY4u9 +5HVL7RK8t8JpZAb7VR+dPhbHEvVpjwuYd5Q05OZ280gFyrhbrKLbqst104GOQT4k +QMJGWxGONyTX6np0Dx6O5jU7dvYvjVVawbJwGuaL6wIDAQABo3oweDAOBgNVHQ8B +Af8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNV +HSMEGDAWgBSu8RGpErgYUoYnQuwCq+/ggTiEjDAiBgNVHREEGzAZgglsb2NhbGhv +c3SCBnBlYmJsZYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAAB0gkekXCNOwqWmY +vQ2lLJ8Zk2WzQ9B+VOC27IgxEEuskZyCpyXAbJB9sCGQWZhAARyaI4SPRGGagcug +d1SwDWdPGeSJzF3aDnXDYoP9Zw2KqiqVZTngeoiw8Yn0F8PNriANwRLybouX7mMc +4V7T5+2k4SUs7pFH4KO0a0XBCcjXDjdKuBljftRTXCHzJzfRtmieCCuZlpnp5sHx +hKa/uxKGyyZB+4Y3MrzsiQSCBOr9G4TH9RofmNcawl+tsVe08zLV/XVhrbakKEs7 +Y7MGHSj3BkPFF32NObc0znqWzTaUD9hU+rXWGANM4sXd4dagdnxfrb7i0WYhcUFj +9Try8Q== -----END CERTIFICATE----- diff --git a/e2e/fixtures/certs/pebble.minica.pem b/e2e/fixtures/certs/pebble.minica.pem index a69a4c419..5578b5b55 100644 --- a/e2e/fixtures/certs/pebble.minica.pem +++ b/e2e/fixtures/certs/pebble.minica.pem @@ -1,19 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDCTCCAfGgAwIBAgIIJOLbes8sTr4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE -AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMTcx -MjA2MTk0MjEwWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGIwggEi +MIIDPzCCAiegAwIBAgIIU0Xm9UFdQxUwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgNTM0NWU2MCAXDTI1MDkwMzIzNDAwNVoYDzIxMjUw +OTAzMjM0MDA1WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA1MzQ1ZTYwggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ alozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn Ajm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu 9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0 toumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3 Hy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB -AAGjRTBDMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB -BQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAF85v -d40HK1ouDAtWeO1PbnWfGEmC5Xa478s9ddOd9Clvp2McYzNlAFfM7kdcj6xeiNhF -WPIfaGAi/QdURSL/6C1KsVDqlFBlTs9zYfh2g0UXGvJtj1maeih7zxFLvet+fqll -xseM4P9EVJaQxwuK/F78YBt0tCNfivC6JNZMgxKF59h0FBpH70ytUSHXdz7FKwix -Mfn3qEb9BXSk0Q3prNV5sOV3vgjEtB4THfDxSz9z3+DepVnW3vbbqwEbkXdk3j82 -2muVldgOUgTwK8eT+XdofVdntzU/kzygSAtAQwLJfn51fS1GvEcYGBc1bDryIqmF -p9BI7gVKtWSZYegicA== +AAGjezB5MA4GA1UdDwEB/wQEAwIChDATBgNVHSUEDDAKBggrBgEFBQcDATASBgNV +HRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSu8RGpErgYUoYnQuwCq+/ggTiEjDAf +BgNVHSMEGDAWgBSu8RGpErgYUoYnQuwCq+/ggTiEjDANBgkqhkiG9w0BAQsFAAOC +AQEAXDVYov1+f6EL7S41LhYQkEX/GyNNzsEvqxE9U0+3Iri5JfkcNOiA9O9L6Z+Y +bqcsXV93s3vi4r4WSWuc//wHyJYrVe5+tK4nlFpbJOvfBUtnoBDyKNxXzZCxFJVh +f9uc8UejRfQMFbDbhWY/x83y9BDufJHHq32OjCIN7gp2UR8rnfYvlz7Zg4qkJBsn +DG4dwd+pRTCFWJOVIG0JoNhK3ZmE7oJ1N4H38XkZ31NPcMksKxpsLLIS9+mosZtg +4olL7tMPJklx5ZaeMFaKRDq4Gdxkbw4+O4vRgNm3Z8AXWKknOdfgdpqLUPPhRcP4 +v1lhy71EhBuXXwRQJry0lTdF+w== -----END CERTIFICATE----- diff --git a/e2e/readme.md b/e2e/readme.md index 7a2367c9b..171170507 100644 --- a/e2e/readme.md +++ b/e2e/readme.md @@ -2,8 +2,8 @@ - Install [Pebble](https://github.com/letsencrypt/pebble): ```bash -go install github.com/letsencrypt/pebble/v2/cmd/pebble@main -go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@main +go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.9.0 +go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.9.0 ``` - Launch tests: diff --git a/go.mod b/go.mod index dd1a5b4b7..b8e88428e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.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 @@ -13,48 +13,49 @@ require ( github.com/Azure/go-autorest/autorest v0.11.30 github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 github.com/Azure/go-autorest/autorest/to v0.4.1 - github.com/BurntSushi/toml v1.5.0 + 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/tea v1.3.14 + 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.5 - github.com/aws/aws-sdk-go-v2/credentials v1.19.5 - 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.94.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.254 + github.com/baidubce/bce-sdk-go v0.9.260 github.com/cenkalti/backoff/v5 v5.0.3 github.com/dnsimple/dnsimple-go/v4 v4.0.0 - github.com/exoscale/egoscale/v3 v3.1.31 + 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.40.3 - github.com/go-acme/tencentclouddnspod v1.1.25 - github.com/go-acme/tencentedgdeone v1.1.48 + 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.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.1.0 + github.com/google/go-querystring v1.2.0 github.com/google/uuid v1.6.0 github.com/gophercloud/gophercloud v1.14.1 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.180 + 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.62.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/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 @@ -64,8 +65,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.1 - github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.1 + 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 @@ -75,34 +76,34 @@ require ( github.com/regfish/regfish-dnsapi-go v0.1.1 github.com/sacloud/api-client-go v0.3.3 github.com/sacloud/iaas-api-go v1.23.1 - github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 + github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 github.com/selectel/domains-go v1.1.0 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.12 + 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/v2 v2.27.7 github.com/vinyldns/go-vinyldns v0.9.17 - github.com/volcengine/volc-sdk-golang v1.0.230 - github.com/vultr/govultr/v3 v3.26.0 - github.com/yandex-cloud/go-genproto v0.41.0 - github.com/yandex-cloud/go-sdk/services/dns v0.0.23 - github.com/yandex-cloud/go-sdk/v2 v2.33.0 - 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 + 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 + 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.257.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.6.0 + software.sslmate.com/src/go-pkcs12 v0.7.0 ) require ( - cloud.google.com/go/auth v0.17.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 @@ -118,22 +119,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.7 // 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/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -151,15 +153,16 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.23.0 // indirect - github.com/go-resty/resty/v2 v2.17.0 // indirect + github.com/go-resty/resty/v2 v2.17.1 // indirect github.com/goccy/go-yaml v1.9.8 // indirect github.com/gofrs/flock v0.13.0 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 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.15.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 @@ -203,23 +206,23 @@ 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.39.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-20251022142026-3a174f9686a8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect - google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect - gopkg.in/ini.v1 v1.67.0 // 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.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5e63fdba3..f5b87c9fe 100644 --- a/go.sum +++ b/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.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +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= @@ -88,8 +88,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= @@ -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= @@ -144,8 +146,8 @@ github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= -github.com/alibabacloud-go/tea v1.3.14 h1:/Uzj5ZCFPpbPR+Bs7jfzsyXkYIVsi5TOIuQNOWwc/9c= -github.com/alibabacloud-go/tea v1.3.14/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= +github.com/alibabacloud-go/tea v1.4.0 h1:MSKhu/kWLPX7mplWMngki8nNt+CyUZ+kfkzaR5VpMhA= +github.com/alibabacloud-go/tea v1.4.0/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= @@ -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.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8= -github.com/aws/aws-sdk-go-v2/config v1.32.5/go.mod h1:xmDjzSUs/d0BB7ClzYPAZMmgQdrodNjPPhd6bGASwoE= -github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hgHYfQFIk1cHDXrR4= -github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk= -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.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.94.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.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/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.254 h1:A7GtBOt7z2lnV7fqlZPZefhcBFg7z6iliUAhEOiIhoE= -github.com/baidubce/bce-sdk-go v0.9.254/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= @@ -239,6 +241,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= @@ -285,8 +289,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/exoscale/egoscale/v3 v3.1.31 h1:/dySEUSAxU+hlAS/eLxAoY8ZYmtOtaoL1P+lDwH7ojY= -github.com/exoscale/egoscale/v3 v3.1.31/go.mod h1:0iY8OxgHJCS5TKqDNhwOW95JBKCnBZl3YGU4Yt+NqkU= +github.com/exoscale/egoscale/v3 v3.1.33 h1:5Lk/pwZ+K0sjNu9obS0VYPfhZQffRkvvO0BpdPoir4o= +github.com/exoscale/egoscale/v3 v3.1.33/go.mod h1:0iY8OxgHJCS5TKqDNhwOW95JBKCnBZl3YGU4Yt+NqkU= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= @@ -315,12 +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.40.3 h1:xXOMRex148wKEHbv7Izn73/HdAxSmz5GOaF4HdnqN+M= -github.com/go-acme/esa-20240910/v2 v2.40.3/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g= -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/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.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= @@ -357,15 +363,15 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-resty/resty/v2 v2.17.0 h1:pW9DeXcaL4Rrym4EZ8v7L19zZiIlWPg5YXAcVmt+gN0= -github.com/go-resty/resty/v2 v2.17.0/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= +github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= +github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= 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= @@ -374,6 +380,8 @@ github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXK github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -440,8 +448,9 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -462,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.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +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= @@ -532,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.180 h1:uia+R3K1izQRGpxTV+bS4q3/ueMW7ProAMWqM6OlqOU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.180/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= @@ -607,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.62.0 h1:eCo1sepsIPGkI66Cz9IaCylWxKDD2aSc5UYq20iBMfw= -github.com/linode/linodego v1.62.0/go.mod h1:FoIEsuZMRlXiUt6RnuGcPTek5iAO3VfE6bjMpGlcQ2U= +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= @@ -644,8 +653,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= @@ -686,8 +695,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= @@ -706,10 +715,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.1 h1:yHD01L6wN7mhGikS08izrMuEp9PRtvingePXkjRHrSg= -github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8= -github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.1 h1:9ApYlc4bjup9WnxOFmgvh00bDqd6KMqAbAR4klKzluA= -github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.1/go.mod h1:iOzhDeDcQGJZVgSDKrl5p3HUWexNo3LOlicf0D9ltgk= +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= @@ -825,8 +834,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/selectel/domains-go v1.1.0 h1:futG50J43ALLKQAnZk9H9yOtLGnSUh7c5hSvuC5gSHo= github.com/selectel/domains-go v1.1.0/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA= @@ -899,10 +908,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.12 h1:/ABtv4x4FSGxGW0d6Sc88iQn6Up2LalWKwt/Tj7Dtz8= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.12/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= @@ -917,10 +926,10 @@ github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= 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.230 h1:84/MOF0zUPtAHt3e1+MsFq5qrnQRC+e3XzTUwIOzZxw= -github.com/volcengine/volc-sdk-golang v1.0.230/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= -github.com/vultr/govultr/v3 v3.26.0 h1:pm/GM+RZo9T1JLQzrUti5HiNAIFZFEHcPFMOWGvvNIY= -github.com/vultr/govultr/v3 v3.26.0/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= @@ -929,12 +938,12 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -github.com/yandex-cloud/go-genproto v0.41.0 h1:l0HWC7S82XgfioqOQ+d2wx7PRB5Eo71KiUb4PiWbDXQ= -github.com/yandex-cloud/go-genproto v0.41.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= -github.com/yandex-cloud/go-sdk/services/dns v0.0.23 h1:fR4tqSRKTpzh4RczXJbU7EOXh4+kJnp+dccRpL2PLPQ= -github.com/yandex-cloud/go-sdk/services/dns v0.0.23/go.mod h1:Lgly3dyKBGrAIpIo6nrkEpQOoSQYlnik1HLKMeZcA98= -github.com/yandex-cloud/go-sdk/v2 v2.33.0 h1:wuvpirhYcHSejLDXSxLGsNoZHqkjrXevzVxw7SrrN/0= -github.com/yandex-cloud/go-sdk/v2 v2.33.0/go.mod h1:OqkwauVaBxbrrfN+JOYBIuE8GrHz1g0Z42VIkbsGvPI= +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= @@ -964,16 +973,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= @@ -1026,8 +1035,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= @@ -1071,8 +1080,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= @@ -1130,16 +1139,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= @@ -1243,8 +1252,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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.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= @@ -1259,8 +1268,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= @@ -1279,8 +1288,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= @@ -1346,8 +1355,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= @@ -1376,8 +1385,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.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= -google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= +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= @@ -1416,12 +1425,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-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +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= @@ -1439,8 +1448,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1455,8 +1464,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1470,11 +1479,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= @@ -1505,5 +1515,5 @@ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU= -software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0= +software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/providers/dns/acmedns/acmedns.toml b/providers/dns/acmedns/acmedns.toml index 6d68a013d..e491569b0 100644 --- a/providers/dns/acmedns/acmedns.toml +++ b/providers/dns/acmedns/acmedns.toml @@ -8,13 +8,13 @@ Since = "v1.1.0" Example = ''' ACME_DNS_API_BASE=http://10.0.0.8:4443 \ ACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \ -lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run +lego --dns "acme-dns" -d '*.example.com' -d example.com run # or ACME_DNS_API_BASE=http://10.0.0.8:4443 \ ACME_DNS_STORAGE_BASE_URL=http://10.10.10.10:80 \ -lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run +lego --dns "acme-dns" -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/active24/active24.toml b/providers/dns/active24/active24.toml index 6a54d4695..b0eaabab8 100644 --- a/providers/dns/active24/active24.toml +++ b/providers/dns/active24/active24.toml @@ -7,7 +7,7 @@ Since = "v4.23.0" Example = ''' ACTIVE24_API_KEY="xxx" \ ACTIVE24_SECRET="yyy" \ -lego --email you@example.com --dns active24 -d '*.example.com' -d example.com run +lego --dns active24 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/alidns/alidns.go b/providers/dns/alidns/alidns.go index a5c883fcb..cdd8e75e0 100644 --- a/providers/dns/alidns/alidns.go +++ b/providers/dns/alidns/alidns.go @@ -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 { @@ -254,12 +257,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) { diff --git a/providers/dns/alidns/alidns.toml b/providers/dns/alidns/alidns.toml index 49a9aeeab..b78e1859d 100644 --- a/providers/dns/alidns/alidns.toml +++ b/providers/dns/alidns/alidns.toml @@ -7,13 +7,13 @@ Since = "v1.1.0" Example = ''' # Setup using instance RAM role ALICLOUD_RAM_ROLE=lego \ -lego --email you@example.com --dns alidns -d '*.example.com' -d example.com run +lego --dns alidns -d '*.example.com' -d example.com run # Or, using credentials ALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ ALICLOUD_SECRET_KEY=your-secret-key \ ALICLOUD_SECURITY_TOKEN=your-sts-token \ -lego --email you@example.com --dns alidns - -d '*.example.com' -d example.com run +lego --dns alidns - -d '*.example.com' -d example.com run ''' [Configuration] @@ -23,6 +23,8 @@ lego --email you@example.com --dns alidns - -d '*.example.com' -d example.com ru 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)" diff --git a/providers/dns/aliesa/aliesa.toml b/providers/dns/aliesa/aliesa.toml index d0f6cdb91..5e7345e40 100644 --- a/providers/dns/aliesa/aliesa.toml +++ b/providers/dns/aliesa/aliesa.toml @@ -7,13 +7,13 @@ Since = "v4.29.0" Example = ''' # Setup using instance RAM role ALIESA_RAM_ROLE=lego \ -lego --email you@example.com --dns aliesa -d '*.example.com' -d example.com run +lego --dns aliesa -d '*.example.com' -d example.com run # Or, using credentials ALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ ALIESA_SECRET_KEY=your-secret-key \ ALIESA_SECURITY_TOKEN=your-sts-token \ -lego --email you@example.com --dns aliesa - -d '*.example.com' -d example.com run +lego --dns aliesa - -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/allinkl/allinkl.go b/providers/dns/allinkl/allinkl.go index 7e8f5ab4e..376b0903c 100644 --- a/providers/dns/allinkl/allinkl.go +++ b/providers/dns/allinkl/allinkl.go @@ -11,6 +11,7 @@ import ( "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/allinkl/internal" "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" @@ -121,20 +122,20 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("allinkl: could not find zone for domain %q: %w", domain, err) - } - ctx := context.Background() credential, err := d.identifier.Authentication(ctx, 60, true) if err != nil { - return fmt.Errorf("allinkl: %w", err) + return fmt.Errorf("allinkl: authentication: %w", err) } ctx = 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) @@ -149,7 +150,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { recordID, err := d.client.AddDNSSettings(ctx, record) if err != nil { - return fmt.Errorf("allinkl: %w", err) + return fmt.Errorf("allinkl: add DNS settings: %w", err) } d.recordIDsMu.Lock() @@ -167,7 +168,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { credential, err := d.identifier.Authentication(ctx, 60, true) if err != nil { - return fmt.Errorf("allinkl: %w", err) + return fmt.Errorf("allinkl: authentication: %w", err) } ctx = internal.WithContext(ctx, credential) @@ -183,7 +184,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { _, err = d.client.DeleteDNSSettings(ctx, recordID) if err != nil { - return fmt.Errorf("allinkl: %w", err) + return fmt.Errorf("allinkl: delete DNS settings: %w", err) } d.recordIDsMu.Lock() @@ -192,3 +193,17 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { 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.Infof("get DNS settings zone[%q] %v", z, errG) + continue + } + + return z, nil + } + + return "", fmt.Errorf("unable to find auth zone for '%s'", fqdn) +} diff --git a/providers/dns/allinkl/allinkl.toml b/providers/dns/allinkl/allinkl.toml index d9c937ee1..774f8fb9f 100644 --- a/providers/dns/allinkl/allinkl.toml +++ b/providers/dns/allinkl/allinkl.toml @@ -7,7 +7,7 @@ Since = "v4.5.0" Example = ''' ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ -lego --email you@example.com --dns allinkl -d '*.example.com' -d example.com run +lego --dns allinkl -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/allinkl/allinkl_test.go b/providers/dns/allinkl/allinkl_test.go index b42adce5d..7da47aee4 100644 --- a/providers/dns/allinkl/allinkl_test.go +++ b/providers/dns/allinkl/allinkl_test.go @@ -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/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/allinkl/internal" "github.com/stretchr/testify/require" ) @@ -143,3 +152,108 @@ func TestLiveCleanUp(t *testing.T) { err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.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("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("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/allinkl/internal/client.go b/providers/dns/allinkl/internal/client.go index d747e9b36..d4403cac5 100644 --- a/providers/dns/allinkl/internal/client.go +++ b/providers/dns/allinkl/internal/client.go @@ -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/v4/platform/wait" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" "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 diff --git a/providers/dns/allinkl/internal/client_test.go b/providers/dns/allinkl/internal/client_test.go index 4b111e31c..949f45bf9 100644 --- a/providers/dns/allinkl/internal/client_test.go +++ b/providers/dns/allinkl/internal/client_test.go @@ -2,7 +2,9 @@ package internal import ( "net/http/httptest" + "net/url" "testing" + "time" "github.com/go-acme/lego/v4/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) diff --git a/providers/dns/allinkl/internal/fixtures/auth-request.xml b/providers/dns/allinkl/internal/fixtures/auth-request.xml new file mode 100644 index 000000000..1cba86f10 --- /dev/null +++ b/providers/dns/allinkl/internal/fixtures/auth-request.xml @@ -0,0 +1,7 @@ + + + + {"kas_login":"user","kas_auth_data":"secret","kas_auth_type":"plain","session_lifetime":60,"session_update_lifetime":"Y"} + + + diff --git a/providers/dns/allinkl/internal/fixtures/flood_protection.xml b/providers/dns/allinkl/internal/fixtures/flood_protection.xml new file mode 100644 index 000000000..b8da10fab --- /dev/null +++ b/providers/dns/allinkl/internal/fixtures/flood_protection.xml @@ -0,0 +1,11 @@ + + + + + SOAP-ENV:Server + flood_protection + KasApi + 0.0688529014587 + + + diff --git a/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_not_found.xml b/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_not_found.xml new file mode 100644 index 000000000..478d07a3a --- /dev/null +++ b/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_not_found.xml @@ -0,0 +1,11 @@ + + + + + SOAP-ENV:Server + zone_not_found + KasApi + example.com + + + diff --git a/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_syntax_incorrect.xml b/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_syntax_incorrect.xml new file mode 100644 index 000000000..c77d733db --- /dev/null +++ b/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_syntax_incorrect.xml @@ -0,0 +1,11 @@ + + + + + SOAP-ENV:Server + zone_syntax_incorrect + KasApi + _acme-challenge.example.com + + + diff --git a/providers/dns/allinkl/internal/identity.go b/providers/dns/allinkl/internal/identity.go index ba8d4d90e..e95e78899 100644 --- a/providers/dns/allinkl/internal/identity.go +++ b/providers/dns/allinkl/internal/identity.go @@ -6,14 +6,14 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "strings" "time" "github.com/go-acme/lego/v4/providers/dns/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) } diff --git a/providers/dns/allinkl/internal/identity_test.go b/providers/dns/allinkl/internal/identity_test.go index 7b93b7688..41d092b13 100644 --- a/providers/dns/allinkl/internal/identity_test.go +++ b/providers/dns/allinkl/internal/identity_test.go @@ -3,6 +3,7 @@ package internal import ( "context" "net/http/httptest" + "net/url" "testing" "github.com/go-acme/lego/v4/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) diff --git a/providers/dns/allinkl/internal/types.go b/providers/dns/allinkl/internal/types.go index b0aa9b4ff..51f7065b5 100644 --- a/providers/dns/allinkl/internal/types.go +++ b/providers/dns/allinkl/internal/types.go @@ -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. diff --git a/providers/dns/allinkl/internal/types_api.go b/providers/dns/allinkl/internal/types_api.go index 22f2c32ed..a11f3aac0 100644 --- a/providers/dns/allinkl/internal/types_api.go +++ b/providers/dns/allinkl/internal/types_api.go @@ -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"` diff --git a/providers/dns/alwaysdata/alwaysdata.go b/providers/dns/alwaysdata/alwaysdata.go new file mode 100644 index 000000000..b2e0f3957 --- /dev/null +++ b/providers/dns/alwaysdata/alwaysdata.go @@ -0,0 +1,185 @@ +// Package alwaysdata implements a DNS provider for solving the DNS-01 challenge using Alwaysdata. +package alwaysdata + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/alwaysdata/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "ALWAYSDATA_" + + EnvAPIKey = envNamespace + "API_KEY" + EnvAccount = envNamespace + "ACCOUNT" + + 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 + Account 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 Alwaysdata. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("alwaysdata: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + config.Account = env.GetOrFile(EnvAccount) + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Alwaysdata. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("alwaysdata: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.APIKey, config.Account) + if err != nil { + return nil, fmt.Errorf("alwaysdata: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("alwaysdata: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name) + if err != nil { + return fmt.Errorf("alwaysdata: %w", err) + } + + record := internal.RecordRequest{ + DomainID: zone.ID, + Name: subDomain, + Type: "TXT", + Value: info.Value, + TTL: d.config.TTL, + Annotation: "lego", + } + + err = d.client.AddRecord(ctx, record) + if err != nil { + return fmt.Errorf("alwaysdata: add TXT record: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("alwaysdata: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name) + if err != nil { + return fmt.Errorf("alwaysdata: %w", err) + } + + records, err := d.client.ListRecords(ctx, zone.ID, subDomain) + if err != nil { + return fmt.Errorf("alwaysdata: list records: %w", err) + } + + for _, record := range records { + if record.Type != "TXT" || record.Value != info.Value { + continue + } + + err = d.client.DeleteRecord(ctx, record.ID) + if err != nil { + return fmt.Errorf("alwaysdata: delete TXT record: %w", err) + } + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.Domain, error) { + domains, err := d.client.ListDomains(ctx) + if err != nil { + return nil, fmt.Errorf("list domains: %w", err) + } + + for a := range dns01.UnFqdnDomainsSeq(fqdn) { + for _, domain := range domains { + if a == domain.Name { + return &domain, nil + } + } + } + + return nil, errors.New("domain not found") +} diff --git a/providers/dns/alwaysdata/alwaysdata.toml b/providers/dns/alwaysdata/alwaysdata.toml new file mode 100644 index 000000000..d00c6f032 --- /dev/null +++ b/providers/dns/alwaysdata/alwaysdata.toml @@ -0,0 +1,26 @@ +Name = "Alwaysdata" +Description = '''''' +URL = "https://alwaysdata.com/" +Code = "alwaysdata" +Since = "v4.31.0" + +Example = ''' +ALWAYSDATA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns alwaysdata -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + ALWAYSDATA_API_KEY = "API Key" + [Configuration.Additional] + ALWAYSDATA_ACCOUNT = "Account name" + ALWAYSDATA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + ALWAYSDATA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + ALWAYSDATA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + ALWAYSDATA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://help.alwaysdata.com/en/api/resources/" + APIDocDomains = "https://api.alwaysdata.com/v1/domain/doc/" + APIDocRecords = "https://api.alwaysdata.com/v1/record/doc/" + APIExamples = "https://help.alwaysdata.com/en/api/examples/" diff --git a/providers/dns/alwaysdata/alwaysdata_test.go b/providers/dns/alwaysdata/alwaysdata_test.go new file mode 100644 index 000000000..6084c2ae4 --- /dev/null +++ b/providers/dns/alwaysdata/alwaysdata_test.go @@ -0,0 +1,187 @@ +package alwaysdata + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIKey, EnvAccount).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: "success with an account", + envVars: map[string]string{ + EnvAPIKey: "secret", + EnvAccount: "foo", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "alwaysdata: some credentials information are missing: ALWAYSDATA_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 + account string + expected string + }{ + { + desc: "success", + apiKey: "secret", + }, + { + desc: "success with an account", + apiKey: "secret", + account: "foo", + }, + { + desc: "missing credentials", + expected: "alwaysdata: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + config.Account = test.account + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.APIKey = "secret" + config.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(). + WithBasicAuth("secret", ""), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /domain/", + servermock.ResponseFromInternal("domains.json")). + Route("POST /record/", + servermock.Noop().WithStatusCode(http.StatusCreated), + servermock.CheckRequestJSONBodyFromInternal("record_add-request.json")). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("GET /domain/", + servermock.ResponseFromInternal("domains.json")). + Route("GET /record/", + servermock.ResponseFromInternal("records.json"), + servermock.CheckQueryParameter().Strict(). + With("domain", "132"). + With("name", "_acme-challenge"), + ). + Route("DELETE /record/789/", + servermock.Noop()). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/alwaysdata/internal/client.go b/providers/dns/alwaysdata/internal/client.go new file mode 100644 index 000000000..5db11dcd1 --- /dev/null +++ b/providers/dns/alwaysdata/internal/client.go @@ -0,0 +1,177 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://api.alwaysdata.com/v1" + +// Client the Alwaysdata API client. +type Client struct { + apiKey string + account string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(apiKey, account string) (*Client, error) { + if apiKey == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + apiKey: apiKey, + account: account, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { + endpoint := c.BaseURL.JoinPath("domain", "/") + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var result []Domain + + err = c.do(req, &result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (c *Client) AddRecord(ctx context.Context, record RecordRequest) error { + endpoint := c.BaseURL.JoinPath("record", "/") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) + if err != nil { + return err + } + + err = c.do(req, nil) + if err != nil { + return err + } + + return nil +} + +func (c *Client) DeleteRecord(ctx context.Context, recordID int64) error { + endpoint := c.BaseURL.JoinPath("record", strconv.FormatInt(recordID, 10), "/") + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) ListRecords(ctx context.Context, domainID int64, name string) ([]Record, error) { + endpoint := c.BaseURL.JoinPath("record", "/") + + query := endpoint.Query() + query.Set("domain", strconv.FormatInt(domainID, 10)) + query.Set("name", name) + endpoint.RawQuery = query.Encode() + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var result []Record + + err = c.do(req, &result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (c *Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + user := c.apiKey + + if c.account != "" { + user += "account=" + c.account + } + + req.SetBasicAuth(user, "") + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} diff --git a/providers/dns/alwaysdata/internal/client_test.go b/providers/dns/alwaysdata/internal/client_test.go new file mode 100644 index 000000000..e6a349662 --- /dev/null +++ b/providers/dns/alwaysdata/internal/client_test.go @@ -0,0 +1,124 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" + "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 = clientdebug.Wrap(server.Client()) + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + WithBasicAuth("secret", ""), + ) +} + +func TestClient_ListDomains(t *testing.T) { + client := mockBuilder(). + Route("GET /domain/", + servermock.ResponseFromFixture("domains.json")). + Build(t) + + result, err := client.ListDomains(t.Context()) + require.NoError(t, err) + + expected := []Domain{ + {ID: 132, Name: "example.com", Annotation: "test"}, + {ID: 133, Name: "example.net", IsInternal: true}, + {ID: 134, Name: "example.org"}, + } + + assert.Equal(t, expected, result) +} + +func TestClient_AddRecord(t *testing.T) { + t.Setenv("LEGO_DEBUG_DNS_API_HTTP_CLIENT", "true") + + client := mockBuilder(). + Route("POST /record/", + servermock.Noop().WithStatusCode(http.StatusCreated), + servermock.CheckRequestJSONBodyFromFixture("record_add-request.json")). + Build(t) + + record := RecordRequest{ + DomainID: 132, + Name: "_acme-challenge", + Type: "TXT", + Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + Annotation: "lego", + } + + err := client.AddRecord(t.Context(), record) + require.NoError(t, err) +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("DELETE /record/789/", + servermock.Noop()). + Build(t) + + err := client.DeleteRecord(t.Context(), 789) + require.NoError(t, err) +} + +func TestClient_ListRecords(t *testing.T) { + client := mockBuilder(). + Route("GET /record/", + servermock.ResponseFromFixture("records.json"), + servermock.CheckQueryParameter().Strict(). + With("domain", "132"). + With("name", "_acme-challenge"), + ). + Build(t) + + result, err := client.ListRecords(t.Context(), 132, "_acme-challenge") + require.NoError(t, err) + + expected := []Record{ + { + ID: 789, + Domain: &Domain{ + Href: "/v1/domain/132/", + }, + Type: "TXT", + Name: "_acme-challenge", + Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + Annotation: "lego", + }, + { + ID: 11619270, + Domain: &Domain{ + Href: "/v1/domain/118935/", + }, + Name: "home", + Type: "A", + Value: "149.202.90.65", + TTL: 300, + IsUserDefined: true, + IsActive: true, + }, + } + + assert.Equal(t, expected, result) +} diff --git a/providers/dns/alwaysdata/internal/fixtures/domains.json b/providers/dns/alwaysdata/internal/fixtures/domains.json new file mode 100644 index 000000000..dc34a948f --- /dev/null +++ b/providers/dns/alwaysdata/internal/fixtures/domains.json @@ -0,0 +1,16 @@ +[ + { + "id": 132, + "name": "example.com", + "annotation": "test" + }, + { + "id": 133, + "name": "example.net", + "is_internal": true + }, + { + "id": 134, + "name": "example.org" + } +] diff --git a/providers/dns/alwaysdata/internal/fixtures/record_add-request.json b/providers/dns/alwaysdata/internal/fixtures/record_add-request.json new file mode 100644 index 000000000..5b6db2646 --- /dev/null +++ b/providers/dns/alwaysdata/internal/fixtures/record_add-request.json @@ -0,0 +1,8 @@ +{ + "domain": 132, + "name": "_acme-challenge", + "type": "TXT", + "value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 120, + "annotation": "lego" +} diff --git a/providers/dns/alwaysdata/internal/fixtures/records.json b/providers/dns/alwaysdata/internal/fixtures/records.json new file mode 100644 index 000000000..fa207395a --- /dev/null +++ b/providers/dns/alwaysdata/internal/fixtures/records.json @@ -0,0 +1,28 @@ +[ + { + "id": 789, + "domain": { + "href": "/v1/domain/132/" + }, + "name": "_acme-challenge", + "type": "TXT", + "value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 120, + "annotation": "lego" + }, + { + "id": 11619270, + "domain": { + "href": "/v1/domain/118935/" + }, + "type": "A", + "name": "home", + "value": "149.202.90.65", + "priority": null, + "ttl": 300, + "href": "/v1/record/11619270/", + "annotation": "", + "is_user_defined": true, + "is_active": true + } +] diff --git a/providers/dns/alwaysdata/internal/types.go b/providers/dns/alwaysdata/internal/types.go new file mode 100644 index 000000000..b1e66fa5b --- /dev/null +++ b/providers/dns/alwaysdata/internal/types.go @@ -0,0 +1,33 @@ +package internal + +type RecordRequest struct { + ID int64 `json:"id,omitempty"` + DomainID int64 `json:"domain,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Value string `json:"value,omitempty"` + TTL int `json:"ttl,omitempty"` + Annotation string `json:"annotation,omitempty"` + IsUserDefined bool `json:"is_user_defined,omitempty"` + IsActive bool `json:"is_active,omitempty"` +} + +type Record struct { + ID int64 `json:"id,omitempty"` + Domain *Domain `json:"domain,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` + TTL int `json:"ttl,omitempty"` + Annotation string `json:"annotation,omitempty"` + IsUserDefined bool `json:"is_user_defined,omitempty"` + IsActive bool `json:"is_active,omitempty"` +} + +type Domain struct { + ID int64 `json:"id,omitempty"` + Href string `json:"href,omitempty"` + Name string `json:"name,omitempty"` + IsInternal bool `json:"is_internal,omitempty"` + Annotation string `json:"annotation,omitempty"` +} diff --git a/providers/dns/anexia/anexia.toml b/providers/dns/anexia/anexia.toml index 4fad8ea48..332f0b8b1 100644 --- a/providers/dns/anexia/anexia.toml +++ b/providers/dns/anexia/anexia.toml @@ -6,7 +6,7 @@ Since = "v4.28.0" Example = ''' ANEXIA_TOKEN=xxx \ -lego --email you@example.com --dns anexia -d '*.example.com' -d example.com run +lego --dns anexia -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/artfiles/artfiles.go b/providers/dns/artfiles/artfiles.go new file mode 100644 index 000000000..c918d77f6 --- /dev/null +++ b/providers/dns/artfiles/artfiles.go @@ -0,0 +1,204 @@ +// 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/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/artfiles/internal" + "github.com/go-acme/lego/v4/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(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(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(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(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 +} diff --git a/providers/dns/artfiles/artfiles.toml b/providers/dns/artfiles/artfiles.toml new file mode 100644 index 000000000..00ff12342 --- /dev/null +++ b/providers/dns/artfiles/artfiles.toml @@ -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" diff --git a/providers/dns/artfiles/artfiles_test.go b/providers/dns/artfiles/artfiles_test.go new file mode 100644 index 000000000..42490f10d --- /dev/null +++ b/providers/dns/artfiles/artfiles_test.go @@ -0,0 +1,228 @@ +package artfiles + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(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(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.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("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("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/artfiles/internal/client.go b/providers/dns/artfiles/internal/client.go new file mode 100644 index 000000000..61b350511 --- /dev/null +++ b/providers/dns/artfiles/internal/client.go @@ -0,0 +1,133 @@ +package internal + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://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 +} diff --git a/providers/dns/artfiles/internal/client_test.go b/providers/dns/artfiles/internal/client_test.go new file mode 100644 index 000000000..cc76f06f5 --- /dev/null +++ b/providers/dns/artfiles/internal/client_test.go @@ -0,0 +1,89 @@ +package internal + +import ( + "encoding/json" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("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) +} diff --git a/providers/dns/artfiles/internal/fixtures/domains.txt b/providers/dns/artfiles/internal/fixtures/domains.txt new file mode 100644 index 000000000..b8a1247d2 --- /dev/null +++ b/providers/dns/artfiles/internal/fixtures/domains.txt @@ -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 diff --git a/providers/dns/artfiles/internal/fixtures/get_dns.json b/providers/dns/artfiles/internal/fixtures/get_dns.json new file mode 100644 index 000000000..fa672e0e1 --- /dev/null +++ b/providers/dns/artfiles/internal/fixtures/get_dns.json @@ -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" +} diff --git a/providers/dns/artfiles/internal/fixtures/set_dns.json b/providers/dns/artfiles/internal/fixtures/set_dns.json new file mode 100644 index 000000000..7cacb33e5 --- /dev/null +++ b/providers/dns/artfiles/internal/fixtures/set_dns.json @@ -0,0 +1,4 @@ +{ + "status": "OK", + "error": "" +} diff --git a/providers/dns/artfiles/internal/fixtures/txt_record-multiple.txt b/providers/dns/artfiles/internal/fixtures/txt_record-multiple.txt new file mode 100644 index 000000000..461489c77 --- /dev/null +++ b/providers/dns/artfiles/internal/fixtures/txt_record-multiple.txt @@ -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" diff --git a/providers/dns/artfiles/internal/fixtures/txt_record.txt b/providers/dns/artfiles/internal/fixtures/txt_record.txt new file mode 100644 index 000000000..5a6259b14 --- /dev/null +++ b/providers/dns/artfiles/internal/fixtures/txt_record.txt @@ -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" diff --git a/providers/dns/artfiles/internal/types.go b/providers/dns/artfiles/internal/types.go new file mode 100644 index 000000000..c70ab34da --- /dev/null +++ b/providers/dns/artfiles/internal/types.go @@ -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 +} diff --git a/providers/dns/artfiles/internal/types_test.go b/providers/dns/artfiles/internal/types_test.go new file mode 100644 index 000000000..3b219f39f --- /dev/null +++ b/providers/dns/artfiles/internal/types_test.go @@ -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) +} diff --git a/providers/dns/arvancloud/arvancloud.toml b/providers/dns/arvancloud/arvancloud.toml index e94452a8b..aa5cafb51 100644 --- a/providers/dns/arvancloud/arvancloud.toml +++ b/providers/dns/arvancloud/arvancloud.toml @@ -6,7 +6,7 @@ Since = "v3.8.0" Example = ''' ARVANCLOUD_API_KEY="Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \ -lego --email you@example.com --dns arvancloud -d '*.example.com' -d example.com run +lego --dns arvancloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/auroradns/auroradns.toml b/providers/dns/auroradns/auroradns.toml index e000e015e..59b5e7ab1 100644 --- a/providers/dns/auroradns/auroradns.toml +++ b/providers/dns/auroradns/auroradns.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' AURORA_API_KEY=xxxxx \ AURORA_SECRET=yyyyyy \ -lego --email you@example.com --dns auroradns -d '*.example.com' -d example.com run +lego --dns auroradns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/autodns/autodns.go b/providers/dns/autodns/autodns.go index fc8e793b6..8a9361bc0 100644 --- a/providers/dns/autodns/autodns.go +++ b/providers/dns/autodns/autodns.go @@ -130,7 +130,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { _, err := d.client.AddRecords(context.Background(), info.EffectiveFQDN, records) if err != nil { - return fmt.Errorf("autodns: %w", err) + return fmt.Errorf("autodns: add record: %w", err) } return nil @@ -149,7 +149,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { _, err := d.client.RemoveRecords(context.Background(), info.EffectiveFQDN, records) if err != nil { - return fmt.Errorf("autodns: %w", err) + return fmt.Errorf("autodns: remove record: %w", err) } return nil diff --git a/providers/dns/autodns/autodns.toml b/providers/dns/autodns/autodns.toml index 78015e431..2798d4cee 100644 --- a/providers/dns/autodns/autodns.toml +++ b/providers/dns/autodns/autodns.toml @@ -7,7 +7,7 @@ Since = "v3.2.0" Example = ''' AUTODNS_API_USER=username \ AUTODNS_API_PASSWORD=supersecretpassword \ -lego --email you@example.com --dns autodns -d '*.example.com' -d example.com run +lego --dns autodns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/axelname/axelname.toml b/providers/dns/axelname/axelname.toml index ee348d5d8..1e2ad6e72 100644 --- a/providers/dns/axelname/axelname.toml +++ b/providers/dns/axelname/axelname.toml @@ -7,7 +7,7 @@ Since = "v4.23.0" Example = ''' AXELNAME_NICKNAME="yyy" \ AXELNAME_TOKEN="xxx" \ -lego --email you@example.com --dns axelname -d '*.example.com' -d example.com run +lego --dns axelname -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/azion/azion.toml b/providers/dns/azion/azion.toml index eacfe74a6..52df20ab5 100644 --- a/providers/dns/azion/azion.toml +++ b/providers/dns/azion/azion.toml @@ -6,7 +6,7 @@ URL = "https://www.azion.com/en/products/edge-dns/" Example = ''' AZION_PERSONAL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns azion -d '*.example.com' -d example.com run +lego --dns azion -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/azure/azure.go b/providers/dns/azure/azure.go index fd00bcbe2..8bfc6cfe1 100644 --- a/providers/dns/azure/azure.go +++ b/providers/dns/azure/azure.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/url" + "strings" "time" "github.com/Azure/go-autorest/autorest" @@ -37,6 +38,8 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) +const EnvLegoAzureBypassDeprecation = "LEGO_AZURE_BYPASS_DEPRECATION" + const defaultMetadataEndpoint = "http://169.254.169.254" var _ challenge.ProviderTimeout = (*DNSProvider)(nil) @@ -133,6 +136,18 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("azure: the configuration of the DNS provider is nil") } + if !env.GetOrDefaultBool(EnvLegoAzureBypassDeprecation, false) { + var msg strings.Builder + + msg.WriteString("azure: ") + msg.WriteString("The `azure` provider has been deprecated since 2023, and replaced by `azuredns` provider. ") + msg.WriteString("It can be TEMPORARILY reactivated by using the environment variable `LEGO_AZURE_BYPASS_DEPRECATION=true`. ") + msg.WriteString("The `azure` provider will be removed in a future release, please migrate to the `azuredns` provider. ") + msg.WriteString("The documentation of the `azuredns` provider can be found at https://go-acme.github.io/lego/dns/azuredns/") + + return nil, errors.New(msg.String()) + } + if config.HTTPClient == nil { config.HTTPClient = &http.Client{Timeout: 5 * time.Second} } diff --git a/providers/dns/azure/azure_test.go b/providers/dns/azure/azure_test.go index 44fb81eef..c4fec4359 100644 --- a/providers/dns/azure/azure_test.go +++ b/providers/dns/azure/azure_test.go @@ -14,6 +14,7 @@ import ( const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( + EnvLegoAzureBypassDeprecation, EnvEnvironment, EnvClientID, EnvClientSecret, @@ -57,6 +58,8 @@ func TestNewDNSProvider(t *testing.T) { envTest.ClearEnv() + test.envVars[EnvLegoAzureBypassDeprecation] = "true" + envTest.Apply(test.envVars) p, err := NewDNSProvider() @@ -140,6 +143,11 @@ func TestNewDNSProviderConfig(t *testing.T) { }, } + defer envTest.RestoreEnv() + + envTest.ClearEnv() + envTest.Apply(map[string]string{EnvLegoAzureBypassDeprecation: "true"}) + for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() diff --git a/providers/dns/azuredns/azuredns.toml b/providers/dns/azuredns/azuredns.toml index 6c1e1ccff..7c800ce7e 100644 --- a/providers/dns/azuredns/azuredns.toml +++ b/providers/dns/azuredns/azuredns.toml @@ -10,32 +10,32 @@ Example = ''' AZURE_CLIENT_ID= \ AZURE_TENANT_ID= \ AZURE_CLIENT_SECRET= \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using client certificate AZURE_CLIENT_ID= \ AZURE_TENANT_ID= \ AZURE_CLIENT_CERTIFICATE_PATH= \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using Azure CLI az login \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using Managed Identity (Azure VM) AZURE_TENANT_ID= \ AZURE_RESOURCE_GROUP= \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using Managed Identity (Azure Arc) AZURE_TENANT_ID= \ IMDS_ENDPOINT=http://localhost:40342 \ IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ''' diff --git a/providers/dns/baiducloud/baiducloud.toml b/providers/dns/baiducloud/baiducloud.toml index 8422eafd5..54f1f6312 100644 --- a/providers/dns/baiducloud/baiducloud.toml +++ b/providers/dns/baiducloud/baiducloud.toml @@ -7,7 +7,7 @@ Since = "v4.23.0" Example = ''' BAIDUCLOUD_ACCESS_KEY_ID="xxx" \ BAIDUCLOUD_SECRET_ACCESS_KEY="yyy" \ -lego --email you@example.com --dns baiducloud -d '*.example.com' -d example.com run +lego --dns baiducloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/beget/beget.toml b/providers/dns/beget/beget.toml index 3cef2f38c..4ed26d850 100644 --- a/providers/dns/beget/beget.toml +++ b/providers/dns/beget/beget.toml @@ -7,7 +7,7 @@ Since = "v4.27.0" Example = ''' BEGET_USERNAME=xxxxxx \ BEGET_PASSWORD=yyyyyy \ -lego --email you@example.com --dns beget -d '*.example.com' -d example.com run +lego --dns beget -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/binarylane/binarylane.toml b/providers/dns/binarylane/binarylane.toml index 5038fc3e6..8b382f3b2 100644 --- a/providers/dns/binarylane/binarylane.toml +++ b/providers/dns/binarylane/binarylane.toml @@ -6,7 +6,7 @@ Since = "v4.26.0" Example = ''' BINARYLANE_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns binarylane -d '*.example.com' -d example.com run +lego --dns binarylane -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/binarylane/internal/types.go b/providers/dns/binarylane/internal/types.go index 987e5c356..06d4be5c0 100644 --- a/providers/dns/binarylane/internal/types.go +++ b/providers/dns/binarylane/internal/types.go @@ -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() diff --git a/providers/dns/bindman/bindman.toml b/providers/dns/bindman/bindman.toml index 5c69e18ff..768601588 100644 --- a/providers/dns/bindman/bindman.toml +++ b/providers/dns/bindman/bindman.toml @@ -6,7 +6,7 @@ Since = "v2.6.0" Example = ''' BINDMAN_MANAGER_ADDRESS= \ -lego --email you@example.com --dns bindman -d '*.example.com' -d example.com run +lego --dns bindman -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/bluecat/bluecat.toml b/providers/dns/bluecat/bluecat.toml index a01a5918d..15df6ed34 100644 --- a/providers/dns/bluecat/bluecat.toml +++ b/providers/dns/bluecat/bluecat.toml @@ -11,7 +11,7 @@ BLUECAT_USER_NAME=myusername \ BLUECAT_CONFIG_NAME=myconfig \ BLUECAT_SERVER_URL=https://bam.example.com \ BLUECAT_TTL=30 \ -lego --email you@example.com --dns bluecat -d '*.example.com' -d example.com run +lego --dns bluecat -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/bluecatv2/bluecatv2.go b/providers/dns/bluecatv2/bluecatv2.go new file mode 100644 index 000000000..0efe99661 --- /dev/null +++ b/providers/dns/bluecatv2/bluecatv2.go @@ -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/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/bluecatv2/internal" + "github.com/go-acme/lego/v4/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(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx, err := d.client.CreateAuthenticatedContext(context.Background()) + if err != nil { + return fmt.Errorf("bluecatv2: %w", err) + } + + zone, err := d.findZone(ctx, 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(ctx, 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(ctx, 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(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(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) + } + + ctx, err := d.client.CreateAuthenticatedContext(context.Background()) + if err != nil { + return fmt.Errorf("bluecatv2: %w", err) + } + + err = d.client.DeleteResourceRecord(ctx, recordID) + if err != nil { + return fmt.Errorf("bluecatv2: delete resource record: %w", err) + } + + if d.config.SkipDeploy { + return nil + } + + _, err = d.client.CreateZoneDeployment(ctx, 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) +} diff --git a/providers/dns/bluecatv2/bluecatv2.toml b/providers/dns/bluecatv2/bluecatv2.toml new file mode 100644 index 000000000..6ec3781c6 --- /dev/null +++ b/providers/dns/bluecatv2/bluecatv2.toml @@ -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" diff --git a/providers/dns/bluecatv2/bluecatv2_test.go b/providers/dns/bluecatv2/bluecatv2_test.go new file mode 100644 index 000000000..d852f0e18 --- /dev/null +++ b/providers/dns/bluecatv2/bluecatv2_test.go @@ -0,0 +1,414 @@ +package bluecatv2 + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/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(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + + config.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("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("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("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("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/bluecatv2/internal/client.go b/providers/dns/bluecatv2/internal/client.go new file mode 100644 index 000000000..d3c801154 --- /dev/null +++ b/providers/dns/bluecatv2/internal/client.go @@ -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/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/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 +} diff --git a/providers/dns/bluecatv2/internal/client_test.go b/providers/dns/bluecatv2/internal/client_test.go new file mode 100644 index 000000000..2559af66e --- /dev/null +++ b/providers/dns/bluecatv2/internal/client_test.go @@ -0,0 +1,208 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/go-acme/lego/v4/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) +} diff --git a/providers/dns/bluecatv2/internal/fixtures/deleteResourceRecord.json b/providers/dns/bluecatv2/internal/fixtures/deleteResourceRecord.json new file mode 100644 index 000000000..38ae2db6e --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/deleteResourceRecord.json @@ -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." +} diff --git a/providers/dns/bluecatv2/internal/fixtures/error.json b/providers/dns/bluecatv2/internal/fixtures/error.json new file mode 100644 index 000000000..d3d2b8b5f --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/error.json @@ -0,0 +1,6 @@ +{ + "status": 401, + "reason": "Unauthorized", + "code": "InvalidAuthorizationToken", + "message": "The provided authorization token is invalid" +} diff --git a/providers/dns/bluecatv2/internal/fixtures/getZoneDeployments.json b/providers/dns/bluecatv2/internal/fixtures/getZoneDeployments.json new file mode 100644 index 000000000..b1a4938ad --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/getZoneDeployments.json @@ -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" + } + ] +} diff --git a/providers/dns/bluecatv2/internal/fixtures/postSession-request.json b/providers/dns/bluecatv2/internal/fixtures/postSession-request.json new file mode 100644 index 000000000..e62048eb9 --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/postSession-request.json @@ -0,0 +1,4 @@ +{ + "username": "userA", + "password": "secret" +} diff --git a/providers/dns/bluecatv2/internal/fixtures/postSession.json b/providers/dns/bluecatv2/internal/fixtures/postSession.json new file mode 100644 index 000000000..4599ad0ad --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/postSession.json @@ -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" + } + } +} diff --git a/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment-request.json b/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment-request.json new file mode 100644 index 000000000..099573a84 --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment-request.json @@ -0,0 +1,3 @@ +{ + "type": "QuickDeployment" +} diff --git a/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment.json b/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment.json new file mode 100644 index 000000000..fd26781fb --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment.json @@ -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" +} diff --git a/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord-request.json b/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord-request.json new file mode 100644 index 000000000..2de733c71 --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord-request.json @@ -0,0 +1,7 @@ +{ + "type": "TXTRecord", + "name": "_acme-challenge", + "ttl": 120, + "recordType": "TXT", + "text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" +} diff --git a/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord.json b/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord.json new file mode 100644 index 000000000..78d028ee3 --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord.json @@ -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" + } +} diff --git a/providers/dns/bluecatv2/internal/fixtures/zones.json b/providers/dns/bluecatv2/internal/fixtures/zones.json new file mode 100644 index 000000000..b9f2dfa8f --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/zones.json @@ -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" + } + ] +} diff --git a/providers/dns/bluecatv2/internal/identity.go b/providers/dns/bluecatv2/internal/identity.go new file mode 100644 index 000000000..af9355ab2 --- /dev/null +++ b/providers/dns/bluecatv2/internal/identity.go @@ -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 +} diff --git a/providers/dns/bluecatv2/internal/identity_test.go b/providers/dns/bluecatv2/internal/identity_test.go new file mode 100644 index 000000000..3a1c4d2a2 --- /dev/null +++ b/providers/dns/bluecatv2/internal/identity_test.go @@ -0,0 +1,82 @@ +package internal + +import ( + "context" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(server.URL, "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)) +} diff --git a/providers/dns/bluecatv2/internal/predicates.go b/providers/dns/bluecatv2/internal/predicates.go new file mode 100644 index 000000000..8ed6f714b --- /dev/null +++ b/providers/dns/bluecatv2/internal/predicates.go @@ -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"} +} diff --git a/providers/dns/bluecatv2/internal/predicates_test.go b/providers/dns/bluecatv2/internal/predicates_test.go new file mode 100644 index 000000000..6913e8729 --- /dev/null +++ b/providers/dns/bluecatv2/internal/predicates_test.go @@ -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()) + }) + } +} diff --git a/providers/dns/bluecatv2/internal/types.go b/providers/dns/bluecatv2/internal/types.go new file mode 100644 index 000000000..562fd60b0 --- /dev/null +++ b/providers/dns/bluecatv2/internal/types.go @@ -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"` +} diff --git a/providers/dns/bookmyname/bookmyname.toml b/providers/dns/bookmyname/bookmyname.toml index 5111c4fbd..76fcb85e7 100644 --- a/providers/dns/bookmyname/bookmyname.toml +++ b/providers/dns/bookmyname/bookmyname.toml @@ -7,7 +7,7 @@ Since = "v4.23.0" Example = ''' BOOKMYNAME_USERNAME="xxx" \ BOOKMYNAME_PASSWORD="yyy" \ -lego --email you@example.com --dns bookmyname -d '*.example.com' -d example.com run +lego --dns bookmyname -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/brandit/brandit.toml b/providers/dns/brandit/brandit.toml index 32d15c15c..4c43e27a9 100644 --- a/providers/dns/brandit/brandit.toml +++ b/providers/dns/brandit/brandit.toml @@ -12,7 +12,7 @@ Since = "v4.11.0" Example = ''' BRANDIT_API_KEY=xxxxxxxxxxxxxxxxxxxxx \ BRANDIT_API_USERNAME=yyyyyyyyyyyyyyyyyyyy \ -lego --email you@example.com --dns brandit -d '*.example.com' -d example.com run +lego --dns brandit -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/bunny/bunny.toml b/providers/dns/bunny/bunny.toml index cbe22d6db..758c4f202 100644 --- a/providers/dns/bunny/bunny.toml +++ b/providers/dns/bunny/bunny.toml @@ -6,7 +6,7 @@ Since = "v4.11.0" Example = ''' BUNNY_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ -lego --email you@example.com --dns bunny -d '*.example.com' -d example.com run +lego --dns bunny -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/checkdomain/checkdomain.toml b/providers/dns/checkdomain/checkdomain.toml index c3ac14e36..0b93058ba 100644 --- a/providers/dns/checkdomain/checkdomain.toml +++ b/providers/dns/checkdomain/checkdomain.toml @@ -6,7 +6,7 @@ Since = "v3.3.0" Example = ''' CHECKDOMAIN_TOKEN=yoursecrettoken \ -lego --email you@example.com --dns checkdomain -d '*.example.com' -d example.com run +lego --dns checkdomain -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/civo/civo.toml b/providers/dns/civo/civo.toml index 9458f01c3..b525712c8 100644 --- a/providers/dns/civo/civo.toml +++ b/providers/dns/civo/civo.toml @@ -6,7 +6,7 @@ Since = "v4.9.0" Example = ''' CIVO_TOKEN=xxxxxx \ -lego --email you@example.com --dns civo -d '*.example.com' -d example.com run +lego --dns civo -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/clouddns/clouddns.toml b/providers/dns/clouddns/clouddns.toml index 154d4da67..6f516e834 100644 --- a/providers/dns/clouddns/clouddns.toml +++ b/providers/dns/clouddns/clouddns.toml @@ -8,7 +8,7 @@ Example = ''' CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \ CLOUDDNS_EMAIL=you@example.com \ CLOUDDNS_PASSWORD=b9841238feb177a84330f \ -lego --email you@example.com --dns clouddns -d '*.example.com' -d example.com run +lego --dns clouddns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/cloudflare/cloudflare.toml b/providers/dns/cloudflare/cloudflare.toml index caf132bb4..c46130fe6 100644 --- a/providers/dns/cloudflare/cloudflare.toml +++ b/providers/dns/cloudflare/cloudflare.toml @@ -7,12 +7,12 @@ Since = "v0.3.0" Example = ''' CLOUDFLARE_EMAIL=you@example.com \ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ -lego --email you@example.com --dns cloudflare -d '*.example.com' -d example.com run +lego --dns cloudflare -d '*.example.com' -d example.com run # or CLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --email you@example.com --dns cloudflare -d '*.example.com' -d example.com run +lego --dns cloudflare -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/cloudflare/internal/types.go b/providers/dns/cloudflare/internal/types.go index 4a7f9e031..50a7bbbf9 100644 --- a/providers/dns/cloudflare/internal/types.go +++ b/providers/dns/cloudflare/internal/types.go @@ -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) } } diff --git a/providers/dns/cloudns/cloudns.toml b/providers/dns/cloudns/cloudns.toml index dd191f06a..ad52ef5b1 100644 --- a/providers/dns/cloudns/cloudns.toml +++ b/providers/dns/cloudns/cloudns.toml @@ -7,7 +7,7 @@ Since = "v2.3.0" Example = ''' CLOUDNS_AUTH_ID=xxxx \ CLOUDNS_AUTH_PASSWORD=yyyy \ -lego --email you@example.com --dns cloudns -d '*.example.com' -d example.com run +lego --dns cloudns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/cloudru/cloudru.toml b/providers/dns/cloudru/cloudru.toml index a6563a3df..b74098a72 100644 --- a/providers/dns/cloudru/cloudru.toml +++ b/providers/dns/cloudru/cloudru.toml @@ -8,7 +8,7 @@ Example = ''' CLOUDRU_SERVICE_INSTANCE_ID=ppp \ CLOUDRU_KEY_ID=xxx \ CLOUDRU_SECRET=yyy \ -lego --email you@example.com --dns cloudru -d '*.example.com' -d example.com run +lego --dns cloudru -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/cloudxns/cloudxns.toml b/providers/dns/cloudxns/cloudxns.toml index e87a741df..32eae8beb 100644 --- a/providers/dns/cloudxns/cloudxns.toml +++ b/providers/dns/cloudxns/cloudxns.toml @@ -9,7 +9,7 @@ Since = "v0.5.0" Example = ''' CLOUDXNS_API_KEY=xxxx \ CLOUDXNS_SECRET_KEY=yyyy \ -lego --email you@example.com --dns cloudxns -d '*.example.com' -d example.com run +lego --dns cloudxns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/com35/com35.go b/providers/dns/com35/com35.go new file mode 100644 index 000000000..4a9de3a18 --- /dev/null +++ b/providers/dns/com35/com35.go @@ -0,0 +1,104 @@ +// Package com35 implements a DNS provider for solving the DNS-01 challenge using 35.com/三五互联. +package com35 + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/internal/westcn" +) + +// Environment variables names. +const ( + envNamespace = "COM35_" + + EnvUsername = envNamespace + "USERNAME" + EnvPassword = envNamespace + "PASSWORD" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +const defaultBaseURL = "https://api.35.cn/api/v2" + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config = westcn.Config + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 60), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + prv challenge.ProviderTimeout +} + +// NewDNSProvider returns a DNSProvider instance configured for 35.com/三五互联. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvUsername, EnvPassword) + if err != nil { + return nil, fmt.Errorf("35com: %w", err) + } + + config := NewDefaultConfig() + config.Username = values[EnvUsername] + config.Password = values[EnvPassword] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for 35.com/三五互联. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("35com: the configuration of the DNS provider is nil") + } + + provider, err := westcn.NewDNSProviderConfig(config, defaultBaseURL) + if err != nil { + return nil, fmt.Errorf("35com: %w", err) + } + + return &DNSProvider{prv: provider}, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + err := d.prv.Present(domain, token, keyAuth) + if err != nil { + return fmt.Errorf("35com: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + err := d.prv.CleanUp(domain, token, keyAuth) + if err != nil { + return fmt.Errorf("35com: %w", err) + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.prv.Timeout() +} diff --git a/providers/dns/com35/com35.toml b/providers/dns/com35/com35.toml new file mode 100644 index 000000000..386ee0043 --- /dev/null +++ b/providers/dns/com35/com35.toml @@ -0,0 +1,24 @@ +Name = "35.com/三五互联" +Description = '''''' +URL = "https://www.35.cn/" +Code = "com35" +Since = "v4.31.0" + +Example = ''' +COM35_USERNAME="xxx" \ +COM35_PASSWORD="yyy" \ +lego --dns com35 -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + COM35_USERNAME = "Username" + COM35_PASSWORD = "API password" + [Configuration.Additional] + COM35_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" + COM35_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" + COM35_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" + COM35_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://api.35.cn/CustomerCenter/doc/domain_v2.html" diff --git a/providers/dns/com35/com35_test.go b/providers/dns/com35/com35_test.go new file mode 100644 index 000000000..78fd8f829 --- /dev/null +++ b/providers/dns/com35/com35_test.go @@ -0,0 +1,144 @@ +package com35 + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "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: "35com: some credentials information are missing: COM35_USERNAME", + }, + { + desc: "missing password", + envVars: map[string]string{ + EnvUsername: "user", + EnvPassword: "", + }, + expected: "35com: some credentials information are missing: COM35_PASSWORD", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "35com: some credentials information are missing: COM35_USERNAME,COM35_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.prv) + } 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: "35com: credentials missing", + }, + { + desc: "missing password", + username: "user", + expected: "35com: credentials missing", + }, + { + desc: "missing credentials", + expected: "35com: 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.prv) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/conoha/conoha.toml b/providers/dns/conoha/conoha.toml index 8bd83247e..be90acb0d 100644 --- a/providers/dns/conoha/conoha.toml +++ b/providers/dns/conoha/conoha.toml @@ -8,7 +8,7 @@ Example = ''' CONOHA_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ CONOHA_API_USERNAME=xxxx \ CONOHA_API_PASSWORD=yyyy \ -lego --email you@example.com --dns conoha -d '*.example.com' -d example.com run +lego --dns conoha -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/conohav3/conohav3.toml b/providers/dns/conohav3/conohav3.toml index 7608e6742..e2c80259d 100644 --- a/providers/dns/conohav3/conohav3.toml +++ b/providers/dns/conohav3/conohav3.toml @@ -8,7 +8,7 @@ Example = ''' CONOHAV3_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ CONOHAV3_API_USER_ID=xxxx \ CONOHAV3_API_PASSWORD=yyyy \ -lego --email you@example.com --dns conohav3 -d '*.example.com' -d example.com run +lego --dns conohav3 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/constellix/constellix.toml b/providers/dns/constellix/constellix.toml index c4ae0a194..171a0de99 100644 --- a/providers/dns/constellix/constellix.toml +++ b/providers/dns/constellix/constellix.toml @@ -7,7 +7,7 @@ Since = "v3.4.0" Example = ''' CONSTELLIX_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ CONSTELLIX_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ -lego --email you@example.com --dns constellix -d '*.example.com' -d example.com run +lego --dns constellix -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/corenetworks/corenetworks.toml b/providers/dns/corenetworks/corenetworks.toml index 8546d8723..09840bb1b 100644 --- a/providers/dns/corenetworks/corenetworks.toml +++ b/providers/dns/corenetworks/corenetworks.toml @@ -7,7 +7,7 @@ Since = "v4.20.0" Example = ''' CORENETWORKS_LOGIN="xxxx" \ CORENETWORKS_PASSWORD="yyyy" \ -lego --email you@example.com --dns corenetworks -d '*.example.com' -d example.com run +lego --dns corenetworks -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/cpanel/cpanel.toml b/providers/dns/cpanel/cpanel.toml index faed2abe2..b64adf0cf 100644 --- a/providers/dns/cpanel/cpanel.toml +++ b/providers/dns/cpanel/cpanel.toml @@ -10,7 +10,7 @@ Example = ''' CPANEL_USERNAME="yyyy" \ CPANEL_TOKEN="xxxx" \ CPANEL_BASE_URL="https://example.com:2083" \ -lego --email you@example.com --dns cpanel -d '*.example.com' -d example.com run +lego --dns cpanel -d '*.example.com' -d example.com run ## WHM @@ -18,7 +18,7 @@ CPANEL_MODE=whm \ CPANEL_USERNAME="yyyy" \ CPANEL_TOKEN="xxxx" \ CPANEL_BASE_URL="https://example.com:2087" \ -lego --email you@example.com --dns cpanel -d '*.example.com' -d example.com run +lego --dns cpanel -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/czechia/czechia.go b/providers/dns/czechia/czechia.go new file mode 100644 index 000000000..3ff397c35 --- /dev/null +++ b/providers/dns/czechia/czechia.go @@ -0,0 +1,159 @@ +// Package czechia implements a DNS provider for solving the DNS-01 challenge using Czechia. +package czechia + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/czechia/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "CZECHIA_" + + EnvToken = envNamespace + "TOKEN" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Token string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for Czechia. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvToken) + if err != nil { + return nil, fmt.Errorf("czechia: %w", err) + } + + config := NewDefaultConfig() + config.Token = values[EnvToken] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Czechia. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("czechia: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.Token) + if err != nil { + return nil, fmt.Errorf("czechia: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("czechia: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("czechia: %w", err) + } + + record := internal.TXTRecord{ + Hostname: subDomain, + Text: info.Value, + TTL: d.config.TTL, + PublishZone: 1, + } + + err = d.client.AddTXTRecord(ctx, dns01.UnFqdn(authZone), record) + if err != nil { + return fmt.Errorf("czechia: add TXT record: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("czechia: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("czechia: %w", err) + } + + record := internal.TXTRecord{ + Hostname: subDomain, + Text: info.Value, + TTL: d.config.TTL, + PublishZone: 1, + } + + err = d.client.DeleteTXTRecord(ctx, dns01.UnFqdn(authZone), record) + if err != nil { + return fmt.Errorf("czechia: delete TXT record: %w", err) + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/dns/czechia/czechia.toml b/providers/dns/czechia/czechia.toml new file mode 100644 index 000000000..2a66d2054 --- /dev/null +++ b/providers/dns/czechia/czechia.toml @@ -0,0 +1,22 @@ +Name = "Czechia" +Description = '''''' +URL = "https://www.czechia.com/" +Code = "czechia" +Since = "v4.33.0" + +Example = ''' +CZECHIA_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns czechia -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + CZECHIA_TOKEN = "Authorization token" + [Configuration.Additional] + CZECHIA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + CZECHIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + CZECHIA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + CZECHIA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://api.czechia.com/swagger/index.html" diff --git a/providers/dns/czechia/czechia_test.go b/providers/dns/czechia/czechia_test.go new file mode 100644 index 000000000..7d9a2676c --- /dev/null +++ b/providers/dns/czechia/czechia_test.go @@ -0,0 +1,165 @@ +package czechia + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvToken: "secret", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "czechia: some credentials information are missing: CZECHIA_TOKEN", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + token string + expected string + }{ + { + desc: "success", + token: "secret", + }, + { + desc: "missing credentials", + expected: "czechia: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Token = test.token + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.Token = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL, _ = url.Parse(server.URL) + + return p, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With("AuthorizationToken", "secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /DNS/example.com/TXT", + servermock.Noop(), + servermock.CheckRequestJSONBodyFromInternal("add_txt_record-request.json"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("DELETE /DNS/example.com/TXT", + servermock.Noop(), + servermock.CheckRequestJSONBodyFromInternal("add_txt_record-request.json"), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/czechia/internal/client.go b/providers/dns/czechia/internal/client.go new file mode 100644 index 000000000..f3e0e462e --- /dev/null +++ b/providers/dns/czechia/internal/client.go @@ -0,0 +1,124 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://api.czechia.com/api" + +const authorizationTokenHeader = "AuthorizationToken" + +// Client the Czechia API client. +type Client struct { + token string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(token string) (*Client, error) { + if token == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + token: token, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) AddTXTRecord(ctx context.Context, domain string, record TXTRecord) error { + endpoint := c.BaseURL.JoinPath("DNS", domain, "TXT") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) DeleteTXTRecord(ctx context.Context, domain string, record TXTRecord) error { + endpoint := c.BaseURL.JoinPath("DNS", domain, "TXT") + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, record) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + req.Header.Set(authorizationTokenHeader, c.token) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} diff --git a/providers/dns/czechia/internal/client_test.go b/providers/dns/czechia/internal/client_test.go new file mode 100644 index 000000000..c6f1141c5 --- /dev/null +++ b/providers/dns/czechia/internal/client_test.go @@ -0,0 +1,67 @@ +package internal + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("secret") + if err != nil { + return nil, err + } + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With(authorizationTokenHeader, "secret"), + ) +} + +func TestClient_AddTXTRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /DNS/example.com/TXT", + servermock.Noop(), + servermock.CheckRequestJSONBodyFromFixture("add_txt_record-request.json"), + ). + Build(t) + + record := TXTRecord{ + Hostname: "_acme-challenge", + Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + PublishZone: 1, + } + + err := client.AddTXTRecord(t.Context(), "example.com", record) + require.NoError(t, err) +} + +func TestClient_DeleteTXTRecord(t *testing.T) { + client := mockBuilder(). + Route("DELETE /DNS/example.com/TXT", + servermock.Noop(), + servermock.CheckRequestJSONBodyFromFixture("add_txt_record-request.json"), + ). + Build(t) + + record := TXTRecord{ + Hostname: "_acme-challenge", + Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + PublishZone: 1, + } + + err := client.DeleteTXTRecord(t.Context(), "example.com", record) + require.NoError(t, err) +} diff --git a/providers/dns/czechia/internal/fixtures/add_txt_record-request.json b/providers/dns/czechia/internal/fixtures/add_txt_record-request.json new file mode 100644 index 000000000..ed5830093 --- /dev/null +++ b/providers/dns/czechia/internal/fixtures/add_txt_record-request.json @@ -0,0 +1,6 @@ +{ + "hostName": "_acme-challenge", + "text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 120, + "publishZone": 1 +} diff --git a/providers/dns/czechia/internal/fixtures/delete_txt_record-request.json b/providers/dns/czechia/internal/fixtures/delete_txt_record-request.json new file mode 100644 index 000000000..ed5830093 --- /dev/null +++ b/providers/dns/czechia/internal/fixtures/delete_txt_record-request.json @@ -0,0 +1,6 @@ +{ + "hostName": "_acme-challenge", + "text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 120, + "publishZone": 1 +} diff --git a/providers/dns/czechia/internal/types.go b/providers/dns/czechia/internal/types.go new file mode 100644 index 000000000..f4a9bfef7 --- /dev/null +++ b/providers/dns/czechia/internal/types.go @@ -0,0 +1,8 @@ +package internal + +type TXTRecord struct { + Hostname string `json:"hostName,omitempty"` + Text string `json:"text,omitempty"` + TTL int `json:"ttl,omitempty"` + PublishZone int `json:"publishZone,omitempty"` +} diff --git a/providers/dns/ddnss/ddnss.go b/providers/dns/ddnss/ddnss.go new file mode 100644 index 000000000..381151c55 --- /dev/null +++ b/providers/dns/ddnss/ddnss.go @@ -0,0 +1,130 @@ +// Package ddnss implements a DNS provider for solving the DNS-01 challenge using DynDNS Service. +package ddnss + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/ddnss/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "DDNSS_" + + EnvKey = envNamespace + "KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" + EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Key string + + PropagationTimeout time.Duration + PollingInterval time.Duration + SequenceInterval 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), + SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), + 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 DynDNS Service. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvKey) + if err != nil { + return nil, fmt.Errorf("ddnss: %w", err) + } + + config := NewDefaultConfig() + config.Key = values[EnvKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for DynDNS Service. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("ddnss: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(&internal.Authentication{Key: config.Key}) + if err != nil { + return nil, fmt.Errorf("ddnss: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + err := d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), info.Value) + if err != nil { + return fmt.Errorf("ddnss: add TXT record: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + err := d.client.RemoveTXTRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN)) + if err != nil { + return fmt.Errorf("ddnss: remove TXT record: %w", err) + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Sequential All DNS challenges for this provider will be resolved sequentially. +// Returns the interval between each iteration. +func (d *DNSProvider) Sequential() time.Duration { + return d.config.SequenceInterval +} diff --git a/providers/dns/ddnss/ddnss.toml b/providers/dns/ddnss/ddnss.toml new file mode 100644 index 000000000..0d0a7132c --- /dev/null +++ b/providers/dns/ddnss/ddnss.toml @@ -0,0 +1,23 @@ +Name = "DDnss (DynDNS Service)" +Description = '''''' +URL = "https://ddnss.de/" +Code = "ddnss" +Since = "v4.32.0" + +Example = ''' +DDNSS_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns ddnss -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + DDNSS_KEY = "Update key" + [Configuration.Additional] + DDNSS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + DDNSS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + DDNSS_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" + DDNSS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + DDNSS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://ddnss.de/info.php" diff --git a/providers/dns/ddnss/ddnss_test.go b/providers/dns/ddnss/ddnss_test.go new file mode 100644 index 000000000..5b1d7df58 --- /dev/null +++ b/providers/dns/ddnss/ddnss_test.go @@ -0,0 +1,168 @@ +package ddnss + +import ( + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvKey: "secret", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "ddnss: some credentials information are missing: DDNSS_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 + Key string + expected string + }{ + { + desc: "success", + Key: "secret", + }, + { + desc: "missing credentials", + expected: "ddnss: missing credentials", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Key = test.Key + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.Key = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL = server.URL + + return p, nil + }, + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /", + servermock.ResponseFromInternal("success.html"), + servermock.CheckQueryParameter().Strict(). + With("host", "_acme-challenge.example.com"). + With("key", "secret"). + With("txt", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + With("txtm", "1"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("GET /", + servermock.ResponseFromInternal("success.html"), + servermock.CheckQueryParameter().Strict(). + With("host", "_acme-challenge.example.com"). + With("key", "secret"). + With("txtm", "2"), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/ddnss/internal/client.go b/providers/dns/ddnss/internal/client.go new file mode 100644 index 000000000..a0cf4b4a6 --- /dev/null +++ b/providers/dns/ddnss/internal/client.go @@ -0,0 +1,137 @@ +package internal + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" + "golang.org/x/net/html" +) + +const defaultBaseURL = "https://ddnss.de/upd.php" + +// Client the DDns API client. +type Client struct { + auth *Authentication + + BaseURL string + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(auth *Authentication) (*Client, error) { + if auth == nil { + return nil, errors.New("credentials missing") + } + + err := auth.validate() + if err != nil { + return nil, err + } + + return &Client{ + auth: auth, + BaseURL: defaultBaseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) AddTXTRecord(ctx context.Context, host, value string) error { + return c.update(ctx, map[string]string{ + "host": host, + "txt": value, + "txtm": "1", + }) +} + +func (c *Client) RemoveTXTRecord(ctx context.Context, host string) error { + return c.update(ctx, map[string]string{ + "host": host, + "txtm": "2", + }) +} + +func (c *Client) update(ctx context.Context, params map[string]string) error { + endpoint, err := url.Parse(c.BaseURL) + if err != nil { + return err + } + + query := endpoint.Query() + + for k, v := range params { + query.Set(k, v) + } + + c.auth.set(query) + + endpoint.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return fmt.Errorf("unable to create request: %w", err) + } + + useragent.SetHeader(req.Header) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + content, err := readPage(raw) + if err != nil { + return err + } + + if strings.Contains(content, "Updated 1 hostname.") { + return nil + } + + return fmt.Errorf("unexpected response: %s", content) +} + +func readPage(raw []byte) (string, error) { + page, err := html.Parse(strings.NewReader(string(raw))) + if err != nil { + return "", err + } + + var b strings.Builder + extractText(page, &b) + + return strings.TrimSpace(b.String()), nil +} + +func extractText(n *html.Node, b *strings.Builder) { + if n.Type == html.TextNode { + text := strings.TrimSpace(n.Data) + if text != "" { + b.WriteString(text + " ") + } + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + extractText(c, b) + } +} diff --git a/providers/dns/ddnss/internal/client_test.go b/providers/dns/ddnss/internal/client_test.go new file mode 100644 index 000000000..3faddded0 --- /dev/null +++ b/providers/dns/ddnss/internal/client_test.go @@ -0,0 +1,56 @@ +package internal + +import ( + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(&Authentication{Key: "secret"}) + if err != nil { + return nil, err + } + + client.BaseURL = server.URL + client.HTTPClient = server.Client() + + return client, nil + }, + ) +} + +func TestClient_AddTXTRecord(t *testing.T) { + client := mockBuilder(). + Route("GET /", + servermock.ResponseFromFixture("success.html"), + servermock.CheckQueryParameter().Strict(). + With("host", "_acme-challenge.example.com"). + With("key", "secret"). + With("txt", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + With("txtm", "1"), + ). + Build(t) + + err := client.AddTXTRecord(t.Context(), "_acme-challenge.example.com", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY") + require.NoError(t, err) +} + +func TestClient_RemoveTXTRecord(t *testing.T) { + client := mockBuilder(). + Route("GET /", + servermock.ResponseFromFixture("success.html"), + servermock.CheckQueryParameter().Strict(). + With("host", "_acme-challenge.example.com"). + With("key", "secret"). + With("txtm", "2"), + ). + Build(t) + + err := client.RemoveTXTRecord(t.Context(), "_acme-challenge.example.com") + require.NoError(t, err) +} diff --git a/providers/dns/ddnss/internal/fixtures/error.html b/providers/dns/ddnss/internal/fixtures/error.html new file mode 100644 index 000000000..f0599ad9a --- /dev/null +++ b/providers/dns/ddnss/internal/fixtures/error.html @@ -0,0 +1,12 @@ + + + DDNSS - Kostenloser DynDNS Service : Re-ProutDNS v5.01v + + +

+

Error Occurred While Processing Request :

+
+ - badysys : Der System Parameter ist ungültig.
+ - badauth : Die Authorisation ist fehlgeschlagen. Die Parameter username und/oder password sind falsch.
+ - notfqdn : Hostname fehlt oder ist falsch.
+ diff --git a/providers/dns/ddnss/internal/fixtures/success.html b/providers/dns/ddnss/internal/fixtures/success.html new file mode 100644 index 000000000..f51957334 --- /dev/null +++ b/providers/dns/ddnss/internal/fixtures/success.html @@ -0,0 +1,8 @@ + + + DDNSS - Kostenloser DynDNS Service : Re-ProutDNS v5.01v + + +

+

Updated 1 hostname.

+ diff --git a/providers/dns/ddnss/internal/types.go b/providers/dns/ddnss/internal/types.go new file mode 100644 index 000000000..37d41e076 --- /dev/null +++ b/providers/dns/ddnss/internal/types.go @@ -0,0 +1,39 @@ +package internal + +import ( + "errors" + "net/url" +) + +type Authentication struct { + Username string `url:"user,omitempty"` + Password string `url:"pwd,omitempty"` + Key string `url:"key,omitempty"` +} + +func (a *Authentication) validate() error { + if a.Username == "" && a.Password == "" && a.Key == "" { + return errors.New("missing credentials") + } + + if a.Username != "" && a.Password != "" && a.Key != "" { + return errors.New("only one of username, password or key can be set") + } + + if (a.Username != "" && a.Password == "") || a.Username == "" && a.Password != "" { + return errors.New("username and password must be set together") + } + + return nil +} + +func (a *Authentication) set(query url.Values) { + if a.Key != "" { + query.Set("key", a.Key) + + return + } + + query.Set("user", a.Username) + query.Set("pwd", a.Password) +} diff --git a/providers/dns/derak/derak.toml b/providers/dns/derak/derak.toml index 45d7e1fcf..72f49883a 100644 --- a/providers/dns/derak/derak.toml +++ b/providers/dns/derak/derak.toml @@ -6,7 +6,7 @@ Since = "v4.12.0" Example = ''' DERAK_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns derak -d '*.example.com' -d example.com run +lego --dns derak -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/desec/desec.toml b/providers/dns/desec/desec.toml index a79b38cd3..f7e66ae07 100644 --- a/providers/dns/desec/desec.toml +++ b/providers/dns/desec/desec.toml @@ -6,7 +6,7 @@ Since = "v3.7.0" Example = ''' DESEC_TOKEN=x-xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns desec -d '*.example.com' -d example.com run +lego --dns desec -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/designate/designate.toml b/providers/dns/designate/designate.toml index 3ea6260a6..a36034f64 100644 --- a/providers/dns/designate/designate.toml +++ b/providers/dns/designate/designate.toml @@ -7,7 +7,7 @@ Since = "v2.2.0" Example = ''' # With a `clouds.yaml` OS_CLOUD=my_openstack \ -lego --email you@example.com --dns designate -d '*.example.com' -d example.com run +lego --dns designate -d '*.example.com' -d example.com run # or @@ -16,7 +16,7 @@ OS_REGION_NAME=RegionOne \ OS_PROJECT_ID=23d4522a987d4ab529f722a007c27846 OS_USERNAME=myuser \ OS_PASSWORD=passw0rd \ -lego --email you@example.com --dns designate -d '*.example.com' -d example.com run +lego --dns designate -d '*.example.com' -d example.com run # or @@ -25,7 +25,7 @@ OS_REGION_NAME=RegionOne \ OS_AUTH_TYPE=v3applicationcredential \ OS_APPLICATION_CREDENTIAL_ID=imn74uq0or7dyzz20dwo1ytls4me8dry \ OS_APPLICATION_CREDENTIAL_SECRET=68FuSPSdQqkFQYH5X1OoriEIJOwyLtQ8QSqXZOc9XxFK1A9tzZT6He2PfPw0OMja \ -lego --email you@example.com --dns designate -d '*.example.com' -d example.com run +lego --dns designate -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/digitalocean/digitalocean.toml b/providers/dns/digitalocean/digitalocean.toml index b30d986f2..8f9107c26 100644 --- a/providers/dns/digitalocean/digitalocean.toml +++ b/providers/dns/digitalocean/digitalocean.toml @@ -6,7 +6,7 @@ Since = "v0.3.0" Example = ''' DO_AUTH_TOKEN=xxxxxx \ -lego --email you@example.com --dns digitalocean -d '*.example.com' -d example.com run +lego --dns digitalocean -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/directadmin/directadmin.toml b/providers/dns/directadmin/directadmin.toml index bd1c9316a..294eaca1c 100644 --- a/providers/dns/directadmin/directadmin.toml +++ b/providers/dns/directadmin/directadmin.toml @@ -8,7 +8,7 @@ Example = ''' DIRECTADMIN_API_URL="http://example.com:2222" \ DIRECTADMIN_USERNAME=xxxx \ DIRECTADMIN_PASSWORD=yyy \ -lego --email you@example.com --dns directadmin -d '*.example.com' -d example.com run +lego --dns directadmin -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/dnsexit/dnsexit.go b/providers/dns/dnsexit/dnsexit.go new file mode 100644 index 000000000..ce9373a50 --- /dev/null +++ b/providers/dns/dnsexit/dnsexit.go @@ -0,0 +1,163 @@ +// Package dnsexit implements a DNS provider for solving the DNS-01 challenge using DNSExit. +package dnsexit + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/dnsexit/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "DNSEXIT_" + + 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, 5*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for DNSExit. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("dnsexit: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for DNSExit. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("dnsexit: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.APIKey) + if err != nil { + return nil, fmt.Errorf("dnsexit: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("dnsexit: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("dnsexit: %w", err) + } + + record := internal.Record{ + Type: "TXT", + Name: subDomain, + Content: info.Value, + TTL: toMinutes(d.config.TTL), + } + + err = d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record) + if err != nil { + return fmt.Errorf("dnsexit: add record: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("dnsexit: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("dnsexit: %w", err) + } + + record := internal.Record{ + Type: "TXT", + Name: subDomain, + Content: info.Value, + } + + err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), record) + if err != nil { + return fmt.Errorf("dnsexit: add record: %w", err) + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func toMinutes(seconds int) int { + i := seconds / 60 + if seconds%60 > 0 { + i++ + } + + return i +} diff --git a/providers/dns/dnsexit/dnsexit.toml b/providers/dns/dnsexit/dnsexit.toml new file mode 100644 index 000000000..0d5321835 --- /dev/null +++ b/providers/dns/dnsexit/dnsexit.toml @@ -0,0 +1,22 @@ +Name = "DNSExit" +Description = '''''' +URL = "https://dnsexit.com" +Code = "dnsexit" +Since = "v4.32.0" + +Example = ''' +DNSEXIT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns dnsexit -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + DNSEXIT_API_KEY = "API key" + [Configuration.Additional] + DNSEXIT_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" + DNSEXIT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" + DNSEXIT_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + DNSEXIT_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://dnsexit.com/dns/dns-api/" diff --git a/providers/dns/dnsexit/dnsexit_test.go b/providers/dns/dnsexit/dnsexit_test.go new file mode 100644 index 000000000..31fe61497 --- /dev/null +++ b/providers/dns/dnsexit/dnsexit_test.go @@ -0,0 +1,165 @@ +package dnsexit + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(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: "key", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "dnsexit: some credentials information are missing: DNSEXIT_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: "key", + }, + { + desc: "missing credentials", + expected: "dnsexit: 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(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.APIKey = "secret" + config.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("apikey", "secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /", + servermock.ResponseFromInternal("success.json"), + servermock.CheckRequestJSONBodyFromInternal("add_record-request.json"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("POST /", + servermock.ResponseFromInternal("success.json"), + servermock.CheckRequestJSONBodyFromInternal("delete_record-request.json"), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/dnsexit/internal/client.go b/providers/dns/dnsexit/internal/client.go new file mode 100644 index 000000000..9b0164846 --- /dev/null +++ b/providers/dns/dnsexit/internal/client.go @@ -0,0 +1,156 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://api.dnsexit.com/dns/" + +// Client the DNSExit 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 +} + +// AddRecord adds a record. +// https://dnsexit.com/dns/dns-api/#example-add-spf +// https://dnsexit.com/dns/dns-api/#example-lse +func (c *Client) AddRecord(ctx context.Context, domain string, record Record) error { + payload := APIRequest{ + Domain: domain, + Add: []Record{record}, + } + + req, err := newJSONRequest(ctx, http.MethodPost, c.BaseURL, payload) + if err != nil { + return err + } + + err = c.do(req) + if err != nil { + return err + } + + return nil +} + +// DeleteRecord deletes a record. +// https://dnsexit.com/dns/dns-api/#delete-a-record +func (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) error { + payload := APIRequest{ + Domain: domain, + Delete: []Record{record}, + } + + req, err := newJSONRequest(ctx, http.MethodPost, c.BaseURL, payload) + if err != nil { + return err + } + + err = c.do(req) + if err != nil { + return err + } + + return nil +} + +func (c *Client) do(req *http.Request) error { + useragent.SetHeader(req.Header) + + req.Header.Set("apikey", c.apiKey) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode > http.StatusBadRequest { + return parseError(req, resp) + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + result := &APIResponse{} + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + if result.Code != 0 { + return result + } + + 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 APIResponse + + err := json.Unmarshal(raw, &errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return &errAPI +} diff --git a/providers/dns/dnsexit/internal/client_test.go b/providers/dns/dnsexit/internal/client_test.go new file mode 100644 index 000000000..26ea01203 --- /dev/null +++ b/providers/dns/dnsexit/internal/client_test.go @@ -0,0 +1,111 @@ +package internal + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("secret") + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + client.BaseURL, _ = url.Parse(server.URL) + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With("apikey", "secret"), + ) +} + +func TestClient_AddRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("success.json"), + servermock.CheckRequestJSONBodyFromFixture("add_record-request.json"), + ). + Build(t) + + record := Record{ + Type: "TXT", + Name: "_acme-challenge", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 2, + } + + err := client.AddRecord(context.Background(), "example.com", record) + require.NoError(t, err) +} + +func TestClient_AddRecord_error(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusBadRequest), + ). + Build(t) + + record := Record{ + Type: "TXT", + Name: "_acme-challenge", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 480, + Overwrite: true, + } + + err := client.AddRecord(context.Background(), "example.com", record) + require.Error(t, err) + + require.EqualError(t, err, "JSON Defined Record Type not Supported (code=6)") +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("success.json"), + servermock.CheckRequestJSONBodyFromFixture("delete_record-request.json"), + ). + Build(t) + + record := Record{ + Type: "TXT", + Name: "_acme-challenge", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + } + + err := client.DeleteRecord(context.Background(), "example.com", record) + require.NoError(t, err) +} + +func TestClient_DeleteRecord_error(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusBadRequest), + ). + Build(t) + + record := Record{ + Type: "TXT", + Name: "foo", + Content: "txtTXTtxt", + } + + err := client.DeleteRecord(context.Background(), "example.com", record) + + require.Error(t, err) + + require.EqualError(t, err, "JSON Defined Record Type not Supported (code=6)") +} diff --git a/providers/dns/dnsexit/internal/fixtures/add_record-request.json b/providers/dns/dnsexit/internal/fixtures/add_record-request.json new file mode 100644 index 000000000..6e5e2b520 --- /dev/null +++ b/providers/dns/dnsexit/internal/fixtures/add_record-request.json @@ -0,0 +1,11 @@ +{ + "domain": "example.com", + "add": [ + { + "type": "TXT", + "name": "_acme-challenge", + "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 2 + } + ] +} diff --git a/providers/dns/dnsexit/internal/fixtures/delete_record-request.json b/providers/dns/dnsexit/internal/fixtures/delete_record-request.json new file mode 100644 index 000000000..dcfef9cdf --- /dev/null +++ b/providers/dns/dnsexit/internal/fixtures/delete_record-request.json @@ -0,0 +1,10 @@ +{ + "domain": "example.com", + "delete": [ + { + "type": "TXT", + "name": "_acme-challenge", + "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" + } + ] +} diff --git a/providers/dns/dnsexit/internal/fixtures/error.json b/providers/dns/dnsexit/internal/fixtures/error.json new file mode 100644 index 000000000..9ba835895 --- /dev/null +++ b/providers/dns/dnsexit/internal/fixtures/error.json @@ -0,0 +1,4 @@ +{ + "code": 6, + "message": "JSON Defined Record Type not Supported" +} diff --git a/providers/dns/dnsexit/internal/fixtures/success.json b/providers/dns/dnsexit/internal/fixtures/success.json new file mode 100644 index 000000000..3af47a936 --- /dev/null +++ b/providers/dns/dnsexit/internal/fixtures/success.json @@ -0,0 +1,7 @@ +{ + "code": 0, + "details": [ + "UPDATE Record A example.com. TTL(hh:mm) 08:00 IP 1.1.1.10" + ], + "message": "Success" +} diff --git a/providers/dns/dnsexit/internal/types.go b/providers/dns/dnsexit/internal/types.go new file mode 100644 index 000000000..db254549f --- /dev/null +++ b/providers/dns/dnsexit/internal/types.go @@ -0,0 +1,41 @@ +package internal + +import ( + "fmt" + "strings" +) + +type Record struct { + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Content string `json:"content,omitempty"` + TTL int `json:"ttl,omitempty"` // NOTE: ttl value is in minutes. + Overwrite bool `json:"overwrite,omitempty"` +} + +type APIRequest struct { + Domain string `json:"domain,omitempty"` + Add []Record `json:"add,omitempty"` + Delete []Record `json:"delete,omitempty"` + Update []Record `json:"update,omitempty"` +} + +// https://dnsexit.com/dns/dns-api/#server-reply + +type APIResponse struct { + Code int `json:"code,omitempty"` + Details []string `json:"details,omitempty"` + Message string `json:"message,omitempty"` +} + +func (a APIResponse) Error() string { + msg := new(strings.Builder) + + _, _ = fmt.Fprintf(msg, "%s (code=%d)", a.Message, a.Code) + + for _, detail := range a.Details { + _, _ = fmt.Fprintf(msg, ", %s", detail) + } + + return msg.String() +} diff --git a/providers/dns/dnshomede/dnshomede.toml b/providers/dns/dnshomede/dnshomede.toml index bc52bb6dd..9c3b65277 100644 --- a/providers/dns/dnshomede/dnshomede.toml +++ b/providers/dns/dnshomede/dnshomede.toml @@ -6,10 +6,10 @@ Since = "v4.10.0" Example = ''' DNSHOMEDE_CREDENTIALS=example.org:password \ -lego --email you@example.com --dns dnshomede -d '*.example.com' -d example.com run +lego --dns dnshomede -d '*.example.com' -d example.com run DNSHOMEDE_CREDENTIALS=my.example.org:password1,demo.example.org:password2 \ -lego --email you@example.com --dns dnshomede -d my.example.org -d demo.example.org +lego --dns dnshomede -d my.example.org -d demo.example.org ''' [Configuration] diff --git a/providers/dns/dnsimple/dnsimple.toml b/providers/dns/dnsimple/dnsimple.toml index dcf999136..158fb7011 100644 --- a/providers/dns/dnsimple/dnsimple.toml +++ b/providers/dns/dnsimple/dnsimple.toml @@ -6,7 +6,7 @@ Since = "v0.3.0" Example = ''' DNSIMPLE_OAUTH_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --email you@example.com --dns dnsimple -d '*.example.com' -d example.com run +lego --dns dnsimple -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/dnsmadeeasy/dnsmadeeasy.toml b/providers/dns/dnsmadeeasy/dnsmadeeasy.toml index 11a5f85ac..d71ab5303 100644 --- a/providers/dns/dnsmadeeasy/dnsmadeeasy.toml +++ b/providers/dns/dnsmadeeasy/dnsmadeeasy.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' DNSMADEEASY_API_KEY=xxxxxx \ DNSMADEEASY_API_SECRET=yyyyy \ -lego --email you@example.com --dns dnsmadeeasy -d '*.example.com' -d example.com run +lego --dns dnsmadeeasy -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/dnspod/dnspod.toml b/providers/dns/dnspod/dnspod.toml index a0bf50e31..162685d76 100644 --- a/providers/dns/dnspod/dnspod.toml +++ b/providers/dns/dnspod/dnspod.toml @@ -8,7 +8,7 @@ Since = "v0.4.0" Example = ''' DNSPOD_API_KEY=xxxxxx \ -lego --email you@example.com --dns dnspod -d '*.example.com' -d example.com run +lego --dns dnspod -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/dode/dode.toml b/providers/dns/dode/dode.toml index a96e9ee43..eb629bb3e 100644 --- a/providers/dns/dode/dode.toml +++ b/providers/dns/dode/dode.toml @@ -6,7 +6,7 @@ Since = "v2.4.0" Example = ''' DODE_TOKEN=xxxxxx \ -lego --email you@example.com --dns dode -d '*.example.com' -d example.com run +lego --dns dode -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/domeneshop/domeneshop.toml b/providers/dns/domeneshop/domeneshop.toml index a8d2a1064..b74af598e 100644 --- a/providers/dns/domeneshop/domeneshop.toml +++ b/providers/dns/domeneshop/domeneshop.toml @@ -8,7 +8,7 @@ Since = "v4.3.0" Example = ''' DOMENESHOP_API_TOKEN= \ DOMENESHOP_API_SECRET= \ -lego --email example@example.com --dns domeneshop -d '*.example.com' -d example.com run +lego --dns domeneshop -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/dreamhost/dreamhost.toml b/providers/dns/dreamhost/dreamhost.toml index 4345e9ece..c3a9db360 100644 --- a/providers/dns/dreamhost/dreamhost.toml +++ b/providers/dns/dreamhost/dreamhost.toml @@ -6,7 +6,7 @@ Since = "v1.1.0" Example = ''' DREAMHOST_API_KEY="YOURAPIKEY" \ -lego --email you@example.com --dns dreamhost -d '*.example.com' -d example.com run +lego --dns dreamhost -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/duckdns/duckdns.toml b/providers/dns/duckdns/duckdns.toml index 9c0b3a6be..6866da57c 100644 --- a/providers/dns/duckdns/duckdns.toml +++ b/providers/dns/duckdns/duckdns.toml @@ -6,7 +6,7 @@ Since = "v0.5.0" Example = ''' DUCKDNS_TOKEN=xxxxxx \ -lego --email you@example.com --dns duckdns -d '*.example.com' -d example.com run +lego --dns duckdns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/dyn/dyn.toml b/providers/dns/dyn/dyn.toml index 4b0d3e652..c4b3563e0 100644 --- a/providers/dns/dyn/dyn.toml +++ b/providers/dns/dyn/dyn.toml @@ -8,7 +8,7 @@ Example = ''' DYN_CUSTOMER_NAME=xxxxxx \ DYN_USER_NAME=yyyyy \ DYN_PASSWORD=zzzz \ -lego --email you@example.com --dns dyn -d '*.example.com' -d example.com run +lego --dns dyn -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/dyndnsfree/dyndnsfree.toml b/providers/dns/dyndnsfree/dyndnsfree.toml index dd354fb33..e64bb0080 100644 --- a/providers/dns/dyndnsfree/dyndnsfree.toml +++ b/providers/dns/dyndnsfree/dyndnsfree.toml @@ -7,7 +7,7 @@ Since = "v4.23.0" Example = ''' DYNDNSFREE_USERNAME="xxx" \ DYNDNSFREE_PASSWORD="yyy" \ -lego --email you@example.com --dns dyndnsfree -d '*.example.com' -d example.com run +lego --dns dyndnsfree -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/dynu/dynu.toml b/providers/dns/dynu/dynu.toml index ba59034dd..ae2367087 100644 --- a/providers/dns/dynu/dynu.toml +++ b/providers/dns/dynu/dynu.toml @@ -6,7 +6,7 @@ Since = "v3.5.0" Example = ''' DYNU_API_KEY=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --email you@example.com --dns dynu -d '*.example.com' -d example.com run +lego --dns dynu -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/easydns/easydns.toml b/providers/dns/easydns/easydns.toml index 71521bbd6..307c86a09 100644 --- a/providers/dns/easydns/easydns.toml +++ b/providers/dns/easydns/easydns.toml @@ -7,7 +7,7 @@ Since = "v2.6.0" Example = ''' EASYDNS_TOKEN=xxx \ EASYDNS_KEY=yyy \ -lego --email you@example.com --dns easydns -d '*.example.com' -d example.com run +lego --dns easydns -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/edgecenter/edgecenter.toml b/providers/dns/edgecenter/edgecenter.toml index 0cd4b0cb6..1c9e9b2a9 100644 --- a/providers/dns/edgecenter/edgecenter.toml +++ b/providers/dns/edgecenter/edgecenter.toml @@ -6,7 +6,7 @@ Since = "v4.29.0" Example = ''' EDGECENTER_PERMANENT_API_TOKEN=xxxxx \ -lego --email you@example.com --dns edgecenter -d '*.example.com' -d example.com run +lego --dns edgecenter -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/edgedns/edgedns.toml b/providers/dns/edgedns/edgedns.toml index d40d5cc03..7c7c5b3aa 100644 --- a/providers/dns/edgedns/edgedns.toml +++ b/providers/dns/edgedns/edgedns.toml @@ -12,7 +12,7 @@ AKAMAI_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890ABCDEFG= \ AKAMAI_CLIENT_TOKEN=akab-mnbvcxzlkjhgfdsapoiuytrewq1234567 \ AKAMAI_HOST=akab-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.luna.akamaiapis.net \ AKAMAI_ACCESS_TOKEN=akab-1234567890qwerty-asdfghjklzxcvtnu \ -lego --email you@example.com --dns edgedns -d '*.example.com' -d example.com run +lego --dns edgedns -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/edgeone/edgeone.toml b/providers/dns/edgeone/edgeone.toml index a33af75b2..05b8bc516 100644 --- a/providers/dns/edgeone/edgeone.toml +++ b/providers/dns/edgeone/edgeone.toml @@ -7,7 +7,7 @@ Since = "v4.26.0" Example = ''' EDGEONE_SECRET_ID=abcdefghijklmnopqrstuvwx \ EDGEONE_SECRET_KEY=your-secret-key \ -lego --email you@example.com --dns edgeone -d '*.example.com' -d example.com run +lego --dns edgeone -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/efficientip/efficientip.toml b/providers/dns/efficientip/efficientip.toml index 565c9575b..6e1874319 100644 --- a/providers/dns/efficientip/efficientip.toml +++ b/providers/dns/efficientip/efficientip.toml @@ -9,7 +9,7 @@ EFFICIENTIP_USERNAME="user" \ EFFICIENTIP_PASSWORD="secret" \ EFFICIENTIP_HOSTNAME="ipam.example.org" \ EFFICIENTIP_DNS_NAME="dns.smart" \ -lego --email you@example.com --dns efficientip -d '*.example.com' -d example.com run +lego --dns efficientip -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/epik/epik.toml b/providers/dns/epik/epik.toml index 7b4688609..faf453581 100644 --- a/providers/dns/epik/epik.toml +++ b/providers/dns/epik/epik.toml @@ -6,7 +6,7 @@ Since = "v4.5.0" Example = ''' EPIK_SIGNATURE=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns epik -d '*.example.com' -d example.com run +lego --dns epik -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/eurodns/eurodns.go b/providers/dns/eurodns/eurodns.go new file mode 100644 index 000000000..21ff3c3a9 --- /dev/null +++ b/providers/dns/eurodns/eurodns.go @@ -0,0 +1,197 @@ +// Package eurodns implements a DNS provider for solving the DNS-01 challenge using EuroDNS. +package eurodns + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/eurodns/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "EURODNS_" + + EnvApplicationID = envNamespace + "APP_ID" + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + ApplicationID string + APIKey string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, internal.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for EuroDNS. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvApplicationID, EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("eurodns: %w", err) + } + + config := NewDefaultConfig() + config.ApplicationID = values[EnvApplicationID] + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for EuroDNS. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("eurodns: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.ApplicationID, config.APIKey) + if err != nil { + return nil, fmt.Errorf("eurodns: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("eurodns: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("eurodns: %w", err) + } + + authZone = dns01.UnFqdn(authZone) + + zone, err := d.client.GetZone(ctx, authZone) + if err != nil { + return fmt.Errorf("eurodns: get zone: %w", err) + } + + zone.Records = append(zone.Records, internal.Record{ + Type: "TXT", + Host: subDomain, + TTL: internal.TTLRounder(d.config.TTL), + RData: info.Value, + }) + + validation, err := d.client.ValidateZone(ctx, authZone, zone) + if err != nil { + return fmt.Errorf("eurodns: validate zone: %w", err) + } + + if validation.Report != nil && !validation.Report.IsValid { + return fmt.Errorf("eurodns: validation report: %w", validation.Report) + } + + err = d.client.SaveZone(ctx, authZone, zone) + if err != nil { + return fmt.Errorf("eurodns: save zone: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("eurodns: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("eurodns: %w", err) + } + + authZone = dns01.UnFqdn(authZone) + + zone, err := d.client.GetZone(ctx, authZone) + if err != nil { + return fmt.Errorf("eurodns: get zone: %w", err) + } + + var recordsToKeep []internal.Record + + for _, record := range zone.Records { + if record.Type == "TXT" && record.Host == subDomain && record.RData == info.Value { + continue + } + + recordsToKeep = append(recordsToKeep, record) + } + + zone.Records = recordsToKeep + + validation, err := d.client.ValidateZone(ctx, authZone, zone) + if err != nil { + return fmt.Errorf("eurodns: validate zone: %w", err) + } + + if validation.Report != nil && !validation.Report.IsValid { + return fmt.Errorf("eurodns: validation report: %w", validation.Report) + } + + err = d.client.SaveZone(ctx, authZone, zone) + if err != nil { + return fmt.Errorf("eurodns: save zone: %w", err) + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/dns/eurodns/eurodns.toml b/providers/dns/eurodns/eurodns.toml new file mode 100644 index 000000000..302b15d00 --- /dev/null +++ b/providers/dns/eurodns/eurodns.toml @@ -0,0 +1,24 @@ +Name = "EuroDNS" +Description = '''''' +URL = "https://www.eurodns.com/" +Code = "eurodns" +Since = "v4.33.0" + +Example = ''' +EURODNS_APP_ID="xxx" \ +EURODNS_API_KEY="yyy" \ +lego --dns eurodns -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + EURODNS_APP_ID = "Application ID" + EURODNS_API_KEY = "API key" + [Configuration.Additional] + EURODNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + EURODNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + EURODNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" + EURODNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://docapi.eurodns.com/" diff --git a/providers/dns/eurodns/eurodns_test.go b/providers/dns/eurodns/eurodns_test.go new file mode 100644 index 000000000..abbb4717e --- /dev/null +++ b/providers/dns/eurodns/eurodns_test.go @@ -0,0 +1,215 @@ +package eurodns + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/eurodns/internal" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvApplicationID, EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvApplicationID: "abc", + EnvAPIKey: "secret", + }, + }, + { + desc: "missing application ID", + envVars: map[string]string{ + EnvApplicationID: "", + EnvAPIKey: "secret", + }, + expected: "eurodns: some credentials information are missing: EURODNS_APP_ID", + }, + { + desc: "missing API secret", + envVars: map[string]string{ + EnvApplicationID: "", + EnvAPIKey: "secret", + }, + expected: "eurodns: some credentials information are missing: EURODNS_APP_ID", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "eurodns: some credentials information are missing: EURODNS_APP_ID,EURODNS_API_KEY", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + appID string + apiKey string + expected string + }{ + { + desc: "success", + appID: "abc", + apiKey: "secret", + }, + { + desc: "missing application ID", + expected: "eurodns: credentials missing", + apiKey: "secret", + }, + { + desc: "missing API secret", + expected: "eurodns: credentials missing", + appID: "abc", + }, + { + desc: "missing credentials", + expected: "eurodns: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.ApplicationID = test.appID + config.APIKey = test.apiKey + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.APIKey = "secret" + config.ApplicationID = "abc" + config.HTTPClient = server.Client() + + provider, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + provider.client.BaseURL, _ = url.Parse(server.URL) + + return provider, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With(internal.HeaderAppID, "abc"). + With(internal.HeaderAPIKey, "secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /example.com", + servermock.ResponseFromInternal("zone_get.json"), + ). + Route("POST /example.com/check", + servermock.ResponseFromInternal("zone_add_validate_ok.json"), + servermock.CheckRequestJSONBodyFromInternal("zone_add.json"), + ). + Route("PUT /example.com", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + servermock.CheckRequestJSONBodyFromInternal("zone_add.json"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("GET /example.com", + servermock.ResponseFromInternal("zone_add.json"), + ). + Route("POST /example.com/check", + servermock.ResponseFromInternal("zone_remove.json"), + servermock.CheckRequestJSONBodyFromInternal("zone_remove.json"), + ). + Route("PUT /example.com", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + servermock.CheckRequestJSONBodyFromInternal("zone_remove.json"), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/eurodns/internal/client.go b/providers/dns/eurodns/internal/client.go new file mode 100644 index 000000000..1ebf8d143 --- /dev/null +++ b/providers/dns/eurodns/internal/client.go @@ -0,0 +1,199 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" +) + +const defaultBaseURL = "https://rest-api.eurodns.com/dns-zones/" + +const ( + HeaderAppID = "X-APP-ID" + HeaderAPIKey = "X-API-KEY" +) + +// Client the EuroDNS API client. +type Client struct { + appID string + apiKey string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(appID, apiKey string) (*Client, error) { + if appID == "" || apiKey == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + appID: appID, + apiKey: apiKey, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +// GetZone gets a DNS Zone. +// https://docapi.eurodns.com/#/dnsprovider/getdnszone +func (c *Client) GetZone(ctx context.Context, domain string) (*Zone, error) { + endpoint := c.BaseURL.JoinPath(domain) + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + result := &Zone{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +// SaveZone saves a DNS Zone. +// https://docapi.eurodns.com/#/dnsprovider/savednszone +func (c *Client) SaveZone(ctx context.Context, domain string, zone *Zone) error { + endpoint := c.BaseURL.JoinPath(domain) + + if len(zone.URLForwards) == 0 { + zone.URLForwards = make([]URLForward, 0) + } + + if len(zone.MailForwards) == 0 { + zone.MailForwards = make([]MailForward, 0) + } + + req, err := newJSONRequest(ctx, http.MethodPut, endpoint, zone) + if err != nil { + return err + } + + return c.do(req, nil) +} + +// ValidateZone validates DNS Zone. +// https://docapi.eurodns.com/#/dnsprovider/checkdnszone +func (c *Client) ValidateZone(ctx context.Context, domain string, zone *Zone) (*Zone, error) { + endpoint := c.BaseURL.JoinPath(domain, "check") + + if len(zone.URLForwards) == 0 { + zone.URLForwards = make([]URLForward, 0) + } + + if len(zone.MailForwards) == 0 { + zone.MailForwards = make([]MailForward, 0) + } + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, zone) + if err != nil { + return nil, err + } + + result := &Zone{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (c *Client) do(req *http.Request, result any) error { + req.Header.Set(HeaderAppID, c.appID) + req.Header.Set(HeaderAPIKey, c.apiKey) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var errAPI APIError + + err := json.Unmarshal(raw, &errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return fmt.Errorf("%d: %w", resp.StatusCode, &errAPI) +} + +const DefaultTTL = 600 + +// TTLRounder rounds the given TTL in seconds to the next accepted value. +// Accepted TTL values are: 600, 900, 1800,3600, 7200, 14400, 21600, 43200, 86400, 172800, 432000, 604800. +func TTLRounder(ttl int) int { + for _, validTTL := range []int{DefaultTTL, 900, 1800, 3600, 7200, 14400, 21600, 43200, 86400, 172800, 432000, 604800} { + if ttl <= validTTL { + return validTTL + } + } + + return DefaultTTL +} diff --git a/providers/dns/eurodns/internal/client_test.go b/providers/dns/eurodns/internal/client_test.go new file mode 100644 index 000000000..68d1fda84 --- /dev/null +++ b/providers/dns/eurodns/internal/client_test.go @@ -0,0 +1,310 @@ +package internal + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "slices" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/internal/ptr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("abc", "secret") + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + client.BaseURL, _ = url.Parse(server.URL) + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With(HeaderAppID, "abc"). + With(HeaderAPIKey, "secret"), + ) +} + +func TestClient_GetZone(t *testing.T) { + client := mockBuilder(). + Route("GET /example.com", + servermock.ResponseFromFixture("zone_get.json"), + ). + Build(t) + + zone, err := client.GetZone(context.Background(), "example.com") + require.NoError(t, err) + + expected := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: slices.Concat([]Record{fakeARecord()}), + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + } + + assert.Equal(t, expected, zone) +} + +func TestClient_GetZone_error(t *testing.T) { + client := mockBuilder(). + Route("GET /example.com", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + _, err := client.GetZone(context.Background(), "example.com") + require.Error(t, err) + + require.EqualError(t, err, "401: INVALID_API_KEY: Invalid API Key") +} + +func TestClient_SaveZone(t *testing.T) { + client := mockBuilder(). + Route("PUT /example.com", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + servermock.CheckRequestJSONBodyFromFixture("zone_add.json"), + ). + Build(t) + + record := Record{ + Type: "TXT", + Host: "_acme-challenge", + RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 600, + } + + zone := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord(), record}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + } + + err := client.SaveZone(context.Background(), "example.com", zone) + require.NoError(t, err) +} + +func TestClient_SaveZone_emptyForwards(t *testing.T) { + client := mockBuilder(). + Route("PUT /example.com", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + servermock.CheckRequestJSONBodyFromFixture("zone_add_empty_forwards.json"), + ). + Build(t) + + record := Record{ + Type: "TXT", + Host: "_acme-challenge", + RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 600, + } + + zone := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: slices.Concat([]Record{fakeARecord(), record}), + } + + err := client.SaveZone(context.Background(), "example.com", zone) + require.NoError(t, err) +} + +func TestClient_SaveZone_error(t *testing.T) { + client := mockBuilder(). + Route("PUT /example.com", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + zone := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord()}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + } + + err := client.SaveZone(context.Background(), "example.com", zone) + require.Error(t, err) + + require.EqualError(t, err, "401: INVALID_API_KEY: Invalid API Key") +} + +func TestClient_ValidateZone(t *testing.T) { + client := mockBuilder(). + Route("POST /example.com/check", + servermock.ResponseFromFixture("zone_add_validate_ok.json"), + servermock.CheckRequestJSONBodyFromFixture("zone_add.json"), + ). + Build(t) + + record := Record{ + Type: "TXT", + Host: "_acme-challenge", + RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 600, + } + + zone := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord(), record}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + } + + zone, err := client.ValidateZone(context.Background(), "example.com", zone) + require.NoError(t, err) + + expected := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord(), record}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + Report: &Report{IsValid: true}, + } + + assert.Equal(t, expected, zone) +} + +func TestClient_ValidateZone_report(t *testing.T) { + client := mockBuilder(). + Route("POST /example.com/check", + servermock.ResponseFromFixture("zone_add_validate_ko.json"), + servermock.CheckRequestJSONBodyFromFixture("zone_add.json"), + ). + Build(t) + + record := Record{ + Type: "TXT", + Host: "_acme-challenge", + RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 600, + } + + zone := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord(), record}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + } + + zone, err := client.ValidateZone(context.Background(), "example.com", zone) + require.NoError(t, err) + + expected := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord(), record}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + Report: fakeReport(), + } + + assert.EqualError(t, zone.Report, `record error (ERROR): "120" is not a valid TTL, URL forward error (ERROR): string, mail forward error (ERROR): string, zone error (ERROR): string`) + + assert.Equal(t, expected, zone) +} + +func TestClient_ValidateZone_error(t *testing.T) { + client := mockBuilder(). + Route("POST /example.com/check", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + zone := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord()}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + } + + _, err := client.ValidateZone(context.Background(), "example.com", zone) + require.Error(t, err) + + require.EqualError(t, err, "401: INVALID_API_KEY: Invalid API Key") +} + +func fakeARecord() Record { + return Record{ + ID: 1000, + Type: "A", + Host: "@", + TTL: 600, + RData: "string", + Updated: ptr.Pointer(true), + Locked: ptr.Pointer(true), + IsDynDNS: ptr.Pointer(true), + Proxy: "ON", + } +} + +func fakeURLForward() URLForward { + return URLForward{ + ID: 2000, + ForwardType: "FRAME", + Host: "string", + URL: "string", + Title: "string", + Keywords: "string", + Description: "string", + Updated: ptr.Pointer(true), + } +} + +func fakeMailForward() MailForward { + return MailForward{ + ID: 3000, + Source: "string", + Destination: "string", + Updated: ptr.Pointer(true), + } +} + +func fakeReport() *Report { + return &Report{ + IsValid: false, + RecordErrors: []RecordError{{ + Messages: []string{`"120" is not a valid TTL`}, + Severity: "ERROR", + Record: fakeARecord(), + }}, + URLForwardErrors: []URLForwardError{{ + Messages: []string{"string"}, + Severity: "ERROR", + URLForward: fakeURLForward(), + }}, + MailForwardErrors: []MailForwardError{{ + Messages: []string{"string"}, + MailForward: fakeMailForward(), + Severity: "ERROR", + }}, + ZoneErrors: []ZoneError{{ + Message: "string", + Severity: "ERROR", + Records: []Record{fakeARecord()}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + }}, + } +} diff --git a/providers/dns/eurodns/internal/fixtures/error.json b/providers/dns/eurodns/internal/fixtures/error.json new file mode 100644 index 000000000..82a334598 --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/error.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "code": "INVALID_API_KEY", + "title": "Invalid API Key" + } + ] +} diff --git a/providers/dns/eurodns/internal/fixtures/zone_add.json b/providers/dns/eurodns/internal/fixtures/zone_add.json new file mode 100644 index 000000000..db8142357 --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/zone_add.json @@ -0,0 +1,46 @@ +{ + "name": "example.com", + "domainConnect": true, + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + }, + { + "type": "TXT", + "host": "_acme-challenge", + "ttl": 600, + "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "updated": null, + "locked": null, + "isDynDns": null + } + ], + "urlForwards": [ + { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + } + ], + "mailForwards": [ + { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + } + ] +} diff --git a/providers/dns/eurodns/internal/fixtures/zone_add_empty_forwards.json b/providers/dns/eurodns/internal/fixtures/zone_add_empty_forwards.json new file mode 100644 index 000000000..64f8530c9 --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/zone_add_empty_forwards.json @@ -0,0 +1,28 @@ +{ + "name": "example.com", + "domainConnect": true, + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + }, + { + "type": "TXT", + "host": "_acme-challenge", + "ttl": 600, + "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "updated": null, + "locked": null, + "isDynDns": null + } + ], + "urlForwards": [], + "mailForwards": [] +} diff --git a/providers/dns/eurodns/internal/fixtures/zone_add_validate_ko.json b/providers/dns/eurodns/internal/fixtures/zone_add_validate_ko.json new file mode 100644 index 000000000..e07d42299 --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/zone_add_validate_ko.json @@ -0,0 +1,139 @@ +{ + "name": "example.com", + "domainConnect": true, + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + }, + { + "type": "TXT", + "host": "_acme-challenge", + "ttl": 600, + "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "updated": null, + "locked": null, + "isDynDns": null + } + ], + "urlForwards": [ + { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + } + ], + "mailForwards": [ + { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + } + ], + "report": { + "isValid": false, + "recordErrors": [ + { + "messages": [ + "\"120\" is not a valid TTL" + ], + "record": { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + }, + "severity": "ERROR" + } + ], + "urlForwardErrors": [ + { + "messages": [ + "string" + ], + "urlForward": { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + }, + "severity": "ERROR" + } + ], + "mailForwardErrors": [ + { + "messages": [ + "string" + ], + "mailForward": { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + }, + "severity": "ERROR" + } + ], + "zoneErrors": [ + { + "message": "string", + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + } + ], + "urlForwards": [ + { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + } + ], + "mailForwards": [ + { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + } + ], + "severity": "ERROR" + } + ] + } +} diff --git a/providers/dns/eurodns/internal/fixtures/zone_add_validate_ok.json b/providers/dns/eurodns/internal/fixtures/zone_add_validate_ok.json new file mode 100644 index 000000000..ba0ddfefb --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/zone_add_validate_ok.json @@ -0,0 +1,49 @@ +{ + "name": "example.com", + "domainConnect": true, + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + }, + { + "type": "TXT", + "host": "_acme-challenge", + "ttl": 600, + "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "updated": null, + "locked": null, + "isDynDns": null + } + ], + "urlForwards": [ + { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + } + ], + "mailForwards": [ + { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + } + ], + "report": { + "isValid": true + } +} diff --git a/providers/dns/eurodns/internal/fixtures/zone_get.json b/providers/dns/eurodns/internal/fixtures/zone_get.json new file mode 100644 index 000000000..ebbc8593e --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/zone_get.json @@ -0,0 +1,37 @@ +{ + "name": "example.com", + "domainConnect": true, + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + } + ], + "urlForwards": [ + { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + } + ], + "mailForwards": [ + { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + } + ] +} diff --git a/providers/dns/eurodns/internal/fixtures/zone_remove.json b/providers/dns/eurodns/internal/fixtures/zone_remove.json new file mode 100644 index 000000000..ebbc8593e --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/zone_remove.json @@ -0,0 +1,37 @@ +{ + "name": "example.com", + "domainConnect": true, + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + } + ], + "urlForwards": [ + { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + } + ], + "mailForwards": [ + { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + } + ] +} diff --git a/providers/dns/eurodns/internal/types.go b/providers/dns/eurodns/internal/types.go new file mode 100644 index 000000000..891b02e14 --- /dev/null +++ b/providers/dns/eurodns/internal/types.go @@ -0,0 +1,136 @@ +package internal + +import ( + "fmt" + "strings" +) + +type APIError struct { + Errors []Error `json:"errors"` +} + +func (a *APIError) Error() string { + var msg []string + + for _, e := range a.Errors { + msg = append(msg, fmt.Sprintf("%s: %s", e.Code, e.Title)) + } + + return strings.Join(msg, ", ") +} + +type Error struct { + Code string `json:"code"` + Title string `json:"title"` +} + +type Zone struct { + Name string `json:"name,omitempty"` + DomainConnect bool `json:"domainConnect,omitempty"` + Records []Record `json:"records"` + URLForwards []URLForward `json:"urlForwards"` + MailForwards []MailForward `json:"mailForwards"` + Report *Report `json:"report,omitempty"` +} + +type Record struct { + ID int `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Host string `json:"host,omitempty"` + TTL int `json:"ttl,omitempty"` + RData string `json:"rdata,omitempty"` + Updated *bool `json:"updated"` + Locked *bool `json:"locked"` + IsDynDNS *bool `json:"isDynDns"` + Proxy string `json:"proxy,omitempty"` +} + +type URLForward struct { + ID int `json:"id,omitempty"` + ForwardType string `json:"forwardType,omitempty"` + Host string `json:"host,omitempty"` + URL string `json:"url,omitempty"` + Title string `json:"title,omitempty"` + Keywords string `json:"keywords,omitempty"` + Description string `json:"description,omitempty"` + Updated *bool `json:"updated,omitempty"` +} + +type MailForward struct { + ID int `json:"id,omitempty"` + Source string `json:"source,omitempty"` + Destination string `json:"destination,omitempty"` + Updated *bool `json:"updated,omitempty"` +} + +type Report struct { + IsValid bool `json:"isValid,omitempty"` + RecordErrors []RecordError `json:"recordErrors,omitempty"` + URLForwardErrors []URLForwardError `json:"urlForwardErrors,omitempty"` + MailForwardErrors []MailForwardError `json:"mailForwardErrors,omitempty"` + ZoneErrors []ZoneError `json:"zoneErrors,omitempty"` +} + +func (r *Report) Error() string { + var msg []string + + for _, e := range r.RecordErrors { + msg = append(msg, e.Error()) + } + + for _, e := range r.URLForwardErrors { + msg = append(msg, e.Error()) + } + + for _, e := range r.MailForwardErrors { + msg = append(msg, e.Error()) + } + + for _, e := range r.ZoneErrors { + msg = append(msg, e.Error()) + } + + return strings.Join(msg, ", ") +} + +type RecordError struct { + Messages []string `json:"messages,omitempty"` + Record Record `json:"record"` + Severity string `json:"severity,omitempty"` +} + +func (e *RecordError) Error() string { + return fmt.Sprintf("record error (%s): %s", e.Severity, strings.Join(e.Messages, ", ")) +} + +type URLForwardError struct { + Messages []string `json:"messages,omitempty"` + URLForward URLForward `json:"urlForward"` + Severity string `json:"severity,omitempty"` +} + +func (e *URLForwardError) Error() string { + return fmt.Sprintf("URL forward error (%s): %s", e.Severity, strings.Join(e.Messages, ", ")) +} + +type MailForwardError struct { + Messages []string `json:"messages,omitempty"` + MailForward MailForward `json:"mailForward"` + Severity string `json:"severity,omitempty"` +} + +func (e *MailForwardError) Error() string { + return fmt.Sprintf("mail forward error (%s): %s", e.Severity, strings.Join(e.Messages, ", ")) +} + +type ZoneError struct { + Message string `json:"message,omitempty"` + Records []Record `json:"records,omitempty"` + URLForwards []URLForward `json:"urlForwards,omitempty"` + MailForwards []MailForward `json:"mailForwards,omitempty"` + Severity string `json:"severity,omitempty"` +} + +func (e *ZoneError) Error() string { + return fmt.Sprintf("zone error (%s): %s", e.Severity, e.Message) +} diff --git a/providers/dns/excedo/excedo.go b/providers/dns/excedo/excedo.go new file mode 100644 index 000000000..ae9128b94 --- /dev/null +++ b/providers/dns/excedo/excedo.go @@ -0,0 +1,176 @@ +// Package excedo implements a DNS provider for solving the DNS-01 challenge using Excedo. +package excedo + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/excedo/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "EXCEDO_" + + EnvAPIURL = envNamespace + "API_URL" + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIURL string + APIKey string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 60), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client + + recordsMu sync.Mutex + records map[string]int64 +} + +// NewDNSProvider returns a DNSProvider instance configured for Excedo. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIURL, EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("excedo: %w", err) + } + + config := NewDefaultConfig() + config.APIURL = values[EnvAPIURL] + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Excedo. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("excedo: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.APIURL, config.APIKey) + if err != nil { + return nil, fmt.Errorf("excedo: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + records: make(map[string]int64), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("excedo: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("excedo: %w", err) + } + + record := internal.Record{ + DomainName: dns01.UnFqdn(authZone), + Name: subDomain, + Type: "TXT", + Content: info.Value, + TTL: strconv.Itoa(d.config.TTL), + } + + recordID, err := d.client.AddRecord(ctx, record) + if err != nil { + return fmt.Errorf("excedo: add record: %w", err) + } + + d.recordsMu.Lock() + d.records[token] = recordID + d.recordsMu.Unlock() + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("excedo: could not find zone for domain %q: %w", domain, err) + } + + d.recordsMu.Lock() + recordID, ok := d.records[token] + d.recordsMu.Unlock() + + if !ok { + return fmt.Errorf("excedo: unknown record ID for '%s'", info.EffectiveFQDN) + } + + err = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), strconv.FormatInt(recordID, 10)) + if err != nil { + return fmt.Errorf("excedo: delete record: %w", err) + } + + d.recordsMu.Lock() + delete(d.records, token) + d.recordsMu.Unlock() + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/dns/excedo/excedo.toml b/providers/dns/excedo/excedo.toml new file mode 100644 index 000000000..9f9874c62 --- /dev/null +++ b/providers/dns/excedo/excedo.toml @@ -0,0 +1,24 @@ +Name = "Excedo" +Description = '''''' +URL = "https://excedo.se/" +Code = "excedo" +Since = "v4.33.0" + +Example = ''' +EXCEDO_API_KEY=your-api-key \ +EXCEDO_API_URL=your-base-url \ +lego --dns excedo -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + EXCEDO_API_KEY = "API key" + EXCEDO_API_URL = "API base URL" + [Configuration.Additional] + EXCEDO_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" + EXCEDO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" + EXCEDO_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" + EXCEDO_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "none" diff --git a/providers/dns/excedo/excedo_test.go b/providers/dns/excedo/excedo_test.go new file mode 100644 index 000000000..f2350c035 --- /dev/null +++ b/providers/dns/excedo/excedo_test.go @@ -0,0 +1,210 @@ +package excedo + +import ( + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIURL, EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIURL: "https://example.com", + EnvAPIKey: "secret", + }, + }, + { + desc: "missing the API key", + envVars: map[string]string{ + EnvAPIURL: "https://example.com", + EnvAPIKey: "", + }, + expected: "excedo: some credentials information are missing: EXCEDO_API_KEY", + }, + { + desc: "missing the API URL", + envVars: map[string]string{ + EnvAPIURL: "", + EnvAPIKey: "secret", + }, + expected: "excedo: some credentials information are missing: EXCEDO_API_URL", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "excedo: some credentials information are missing: EXCEDO_API_URL,EXCEDO_API_KEY", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + apiURL string + apiKey string + expected string + }{ + { + desc: "success", + apiURL: "https://example.com", + apiKey: "secret", + }, + { + desc: "missing the API key", + apiURL: "https://example.com", + expected: "excedo: credentials missing", + }, + { + desc: "missing the API URL", + apiKey: "secret", + expected: "excedo: credentials missing", + }, + { + desc: "missing credentials", + expected: "excedo: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIURL = test.apiURL + config.APIKey = test.apiKey + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.APIURL = server.URL + config.APIKey = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + return p, nil + }, + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /authenticate/login/", + servermock.ResponseFromInternal("login.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secret"), + ). + Route("POST /dns/addrecord/", + servermock.ResponseFromInternal("addrecord.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer session-token"), + servermock.CheckForm().Strict(). + With("content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + With("domainName", "example.com"). + With("name", "_acme-challenge"). + With("ttl", "60"). + With("type", "TXT"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("GET /authenticate/login/", + servermock.ResponseFromInternal("login.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secret"), + ). + Route("POST /dns/deleterecord/", + servermock.ResponseFromInternal("deleterecord.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer session-token"), + ). + Build(t) + + provider.records["abc"] = 19695822 + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/excedo/internal/client.go b/providers/dns/excedo/internal/client.go new file mode 100644 index 000000000..a5d8be88b --- /dev/null +++ b/providers/dns/excedo/internal/client.go @@ -0,0 +1,205 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "sync" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" + querystring "github.com/google/go-querystring/query" +) + +type responseChecker interface { + Check() error +} + +// Client the Excedo API client. +type Client struct { + apiKey string + + baseURL *url.URL + HTTPClient *http.Client + + token *ExpirableToken + muToken sync.Mutex +} + +// NewClient creates a new Client. +func NewClient(apiURL, apiKey string) (*Client, error) { + if apiURL == "" || apiKey == "" { + return nil, errors.New("credentials missing") + } + + baseURL, err := url.Parse(apiURL) + if err != nil { + return nil, err + } + + return &Client{ + apiKey: apiKey, + baseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) AddRecord(ctx context.Context, record Record) (int64, error) { + payload, err := querystring.Values(record) + if err != nil { + return 0, err + } + + endpoint := c.baseURL.JoinPath("/dns/addrecord/") + + req, err := newFormRequest(ctx, http.MethodPost, endpoint, payload) + if err != nil { + return 0, err + } + + result := new(AddRecordResponse) + + err = c.doAuthenticated(ctx, req, result) + if err != nil { + return 0, err + } + + return result.RecordID, nil +} + +func (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error { + endpoint := c.baseURL.JoinPath("/dns/deleterecord/") + + data := map[string]string{ + "domainname": dns01.UnFqdn(zone), + "recordid": recordID, + } + + req, err := newMultipartRequest(ctx, http.MethodPost, endpoint, data) + if err != nil { + return err + } + + result := new(BaseResponse) + + err = c.doAuthenticated(ctx, req, result) + if err != nil { + return err + } + + return nil +} + +func (c *Client) GetRecords(ctx context.Context, zone string) (map[string]Zone, error) { + endpoint := c.baseURL.JoinPath("/dns/getrecords/") + + query := endpoint.Query() + query.Set("domainname", zone) + + endpoint.RawQuery = query.Encode() + + req, err := newFormRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + result := new(GetRecordsResponse) + + err = c.doAuthenticated(ctx, req, result) + if err != nil { + return nil, err + } + + return result.DNS, nil +} + +func (c *Client) do(req *http.Request, result responseChecker) error { + useragent.SetHeader(req.Header) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return result.Check() +} + +func newMultipartRequest(ctx context.Context, method string, endpoint *url.URL, data map[string]string) (*http.Request, error) { + buf := new(bytes.Buffer) + + writer := multipart.NewWriter(buf) + + for k, v := range data { + err := writer.WriteField(k, v) + if err != nil { + return nil, err + } + } + + err := writer.Close() + if err != nil { + return nil, err + } + + body := bytes.NewReader(buf.Bytes()) + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + + return req, nil +} + +func newFormRequest(ctx context.Context, method string, endpoint *url.URL, form url.Values) (*http.Request, error) { + var body io.Reader + + if len(form) > 0 { + body = bytes.NewReader([]byte(form.Encode())) + } else { + body = http.NoBody + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + if method == http.MethodPost { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + return req, nil +} diff --git a/providers/dns/excedo/internal/client_test.go b/providers/dns/excedo/internal/client_test.go new file mode 100644 index 000000000..f4fd52c00 --- /dev/null +++ b/providers/dns/excedo/internal/client_test.go @@ -0,0 +1,137 @@ +package internal + +import ( + "net/http/httptest" + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(server.URL, "secret") + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + + return client, nil + }, + ) +} + +func TestClient_AddRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /dns/addrecord/", + servermock.ResponseFromFixture("addrecord.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer session-token"), + servermock.CheckForm().Strict(). + With("content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + With("domainName", "example.com"). + With("name", "_acme-challenge"). + With("ttl", "60"). + With("type", "TXT"), + ). + Build(t) + + client.token = &ExpirableToken{ + Token: "session-token", + Expires: time.Now().Add(6 * time.Hour), + } + + record := Record{ + DomainName: "example.com", + Name: "_acme-challenge", + Type: "TXT", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: "60", + } + + recordID, err := client.AddRecord(t.Context(), record) + require.NoError(t, err) + + assert.EqualValues(t, 19695822, recordID) +} + +func TestClient_AddRecord_error(t *testing.T) { + client := mockBuilder(). + Route("POST /dns/addrecord/", + servermock.ResponseFromFixture("error.json"), + ). + Build(t) + + client.token = &ExpirableToken{ + Token: "session-token", + Expires: time.Now().Add(6 * time.Hour), + } + + record := Record{ + DomainName: "example.com", + Name: "_acme-challenge", + Type: "TXT", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: "60", + } + + _, err := client.AddRecord(t.Context(), record) + require.EqualError(t, err, "2003: Required parameter missing") +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /dns/deleterecord/", + servermock.ResponseFromFixture("deleterecord.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer session-token"), + ). + Build(t) + + client.token = &ExpirableToken{ + Token: "session-token", + Expires: time.Now().Add(6 * time.Hour), + } + + err := client.DeleteRecord(t.Context(), "example.com", "19695822") + require.NoError(t, err) +} + +func TestClient_GetRecords(t *testing.T) { + client := mockBuilder(). + Route("GET /dns/getrecords/", + servermock.ResponseFromFixture("getrecords.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer session-token"), + servermock.CheckQueryParameter().Strict(). + With("domainname", "example.com"), + ). + Build(t) + + client.token = &ExpirableToken{ + Token: "session-token", + Expires: time.Now().Add(6 * time.Hour), + } + + zones, err := client.GetRecords(t.Context(), "example.com") + require.NoError(t, err) + + expected := map[string]Zone{ + "example.com": { + DNSType: "type", + Records: []Record{{ + RecordID: "1234", + Name: "_acme-challenge.example.com", + Type: "TXT", + Content: "txt-value", + TTL: "60", + }}, + }, + } + + assert.Equal(t, expected, zones) +} diff --git a/providers/dns/excedo/internal/fixtures/addrecord.json b/providers/dns/excedo/internal/fixtures/addrecord.json new file mode 100644 index 000000000..f1f7bf958 --- /dev/null +++ b/providers/dns/excedo/internal/fixtures/addrecord.json @@ -0,0 +1,15 @@ +{ + "code": 1000, + "desc": "Command completed successfully", + "recordid": 19695822, + "session": { + "accID": "1234", + "usrID": "1234", + "status": "active", + "expire": { + "date": "2026-03-10 19:03:18", + "seconds": 5678 + } + }, + "runtime": 0.2852 +} diff --git a/providers/dns/excedo/internal/fixtures/deleterecord.json b/providers/dns/excedo/internal/fixtures/deleterecord.json new file mode 100644 index 000000000..5c2431b1c --- /dev/null +++ b/providers/dns/excedo/internal/fixtures/deleterecord.json @@ -0,0 +1,14 @@ +{ + "code": 1000, + "desc": "Command completed successfully", + "session": { + "accID": "1234", + "usrID": "1234", + "status": "active", + "expire": { + "date": "2026-03-10 19:03:18", + "seconds": 5678 + } + }, + "runtime": 0.2852 +} diff --git a/providers/dns/excedo/internal/fixtures/error.json b/providers/dns/excedo/internal/fixtures/error.json new file mode 100644 index 000000000..5a24ec247 --- /dev/null +++ b/providers/dns/excedo/internal/fixtures/error.json @@ -0,0 +1,18 @@ +{ + "code": 2003, + "desc": "Required parameter missing", + "missing": [ + "domainname", + "recordid" + ], + "session": { + "accID": "1234", + "usrID": "1234", + "status": "active", + "expire": { + "date": "2026-03-10 19:03:18", + "seconds": 5485 + } + }, + "runtime": 0.0534 +} diff --git a/providers/dns/excedo/internal/fixtures/getrecords.json b/providers/dns/excedo/internal/fixtures/getrecords.json new file mode 100644 index 000000000..215a8abb2 --- /dev/null +++ b/providers/dns/excedo/internal/fixtures/getrecords.json @@ -0,0 +1,23 @@ +{ + "code": 1000, + "desc": "Command completed successfully", + "dns": { + "example.com": { + "dnstype": "type", + "recordusage": { + "used": 74 + }, + "records": [ + { + "recordid": "1234", + "name": "_acme-challenge.example.com", + "type": "TXT", + "content": "txt-value", + "ttl": "60", + "prio": null, + "change_date": null + } + ] + } + } +} diff --git a/providers/dns/excedo/internal/fixtures/login.json b/providers/dns/excedo/internal/fixtures/login.json new file mode 100644 index 000000000..2defb9843 --- /dev/null +++ b/providers/dns/excedo/internal/fixtures/login.json @@ -0,0 +1,7 @@ +{ + "code": 1000, + "desc": "Command completed successfully", + "parameters": { + "token": "session-token" + } +} diff --git a/providers/dns/excedo/internal/identity.go b/providers/dns/excedo/internal/identity.go new file mode 100644 index 000000000..5c9ca119d --- /dev/null +++ b/providers/dns/excedo/internal/identity.go @@ -0,0 +1,75 @@ +package internal + +import ( + "context" + "fmt" + "net/http" + "time" +) + +type ExpirableToken struct { + Token string + Expires time.Time +} + +func (t *ExpirableToken) IsExpired() bool { + return time.Now().After(t.Expires) +} + +func (c *Client) Login(ctx context.Context) (string, error) { + endpoint := c.baseURL.JoinPath("/authenticate/login/") + + req, err := newFormRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return "", err + } + + req.Header.Set("Authorization", "Bearer "+c.apiKey) + + result := new(LoginResponse) + + err = c.do(req, result) + if err != nil { + return "", err + } + + if result.Code != 1000 && result.Code != 1300 { + return "", fmt.Errorf("%d: %s", result.Code, result.Description) + } + + return result.Parameters.Token, nil +} + +func (c *Client) authenticate(ctx context.Context) (string, error) { + c.muToken.Lock() + defer c.muToken.Unlock() + + if c.token == nil || c.token.IsExpired() { + token, err := c.Login(ctx) + if err != nil { + return "", err + } + + c.token = &ExpirableToken{ + Token: token, + Expires: time.Now().Add(2*time.Hour - time.Minute), + } + + return token, nil + } + + return c.token.Token, nil +} + +func (c *Client) doAuthenticated(ctx context.Context, req *http.Request, result responseChecker) error { + token, err := c.authenticate(ctx) + if err != nil { + return err + } + + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + + return c.do(req, result) +} diff --git a/providers/dns/excedo/internal/identity_test.go b/providers/dns/excedo/internal/identity_test.go new file mode 100644 index 000000000..86b7eb9d8 --- /dev/null +++ b/providers/dns/excedo/internal/identity_test.go @@ -0,0 +1,35 @@ +package internal + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClient_Login(t *testing.T) { + client := mockBuilder(). + Route("GET /authenticate/login/", + servermock.ResponseFromFixture("login.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secret"), + ). + Build(t) + + token, err := client.Login(t.Context()) + require.NoError(t, err) + + assert.Equal(t, "session-token", token) +} + +func TestClient_Login_error(t *testing.T) { + client := mockBuilder(). + Route("GET /authenticate/login/", + servermock.ResponseFromFixture("error.json"), + ). + Build(t) + + _, err := client.Login(t.Context()) + require.EqualError(t, err, "2003: Required parameter missing") +} diff --git a/providers/dns/excedo/internal/types.go b/providers/dns/excedo/internal/types.go new file mode 100644 index 000000000..eb6ce8462 --- /dev/null +++ b/providers/dns/excedo/internal/types.go @@ -0,0 +1,65 @@ +package internal + +import "fmt" + +type BaseResponse struct { + Code int `json:"code"` + Description string `json:"desc"` +} + +func (r BaseResponse) Check() error { + // Response codes: + // - 1000: Command completed successfully + // - 1300: Command completed successfully; no messages + // - 2001: Command syntax error + // - 2002: Command use error + // - 2003: Required parameter missing + // - 2004: Parameter value range error + // - 2104: Billing failure + // - 2200: Authentication error + // - 2201: Authorization error + // - 2303: Object does not exist + // - 2304: Object status prohibits operation + // - 2309: Object duplicate found + // - 2400: Command failed + // - 2500: Command failed; server closing connection + if r.Code != 1000 && r.Code != 1300 { + return fmt.Errorf("%d: %s", r.Code, r.Description) + } + + return nil +} + +type GetRecordsResponse struct { + BaseResponse + + DNS map[string]Zone `json:"dns"` +} + +type Zone struct { + DNSType string `json:"dnstype"` + Records []Record `json:"records"` +} + +type Record struct { + DomainName string `json:"domainName,omitempty" url:"domainName,omitempty"` + RecordID string `json:"recordid,omitempty" url:"recordid,omitempty"` + Name string `json:"name,omitempty" url:"name,omitempty"` + Type string `json:"type,omitempty" url:"type,omitempty"` + Content string `json:"content,omitempty" url:"content,omitempty"` + TTL string `json:"ttl,omitempty" url:"ttl,omitempty"` +} + +type AddRecordResponse struct { + BaseResponse + + RecordID int64 `json:"recordid"` +} + +type LoginResponse struct { + BaseResponse + + Parameters struct { + Token string `json:"token"` + } `json:"parameters"` +} diff --git a/providers/dns/exec/exec.toml b/providers/dns/exec/exec.toml index 4c8d70b1c..2f9c77c67 100644 --- a/providers/dns/exec/exec.toml +++ b/providers/dns/exec/exec.toml @@ -6,7 +6,7 @@ Since = "v0.5.0" Example = ''' EXEC_PATH=/the/path/to/myscript.sh \ -lego --email you@example.com --dns exec -d '*.example.com' -d example.com run +lego --dns exec -d '*.example.com' -d example.com run ''' Additional = ''' @@ -39,7 +39,7 @@ For example, requesting a certificate for the domain 'my.example.org' can be ach ```bash EXEC_PATH=./update-dns.sh \ -lego --email you@example.com --dns exec --d my.example.org run +lego --dns exec --d my.example.org run ``` It will then call the program './update-dns.sh' with like this: @@ -59,7 +59,7 @@ If you want to use the raw domain, token, and keyAuth values with your program, ```bash EXEC_MODE=RAW \ EXEC_PATH=./update-dns.sh \ -lego --email you@example.com --dns exec -d my.example.org run +lego --dns exec -d my.example.org run ``` It will then call the program `./update-dns.sh` like this: diff --git a/providers/dns/exoscale/exoscale.toml b/providers/dns/exoscale/exoscale.toml index 82c005d26..bcc912b07 100644 --- a/providers/dns/exoscale/exoscale.toml +++ b/providers/dns/exoscale/exoscale.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' EXOSCALE_API_KEY=abcdefghijklmnopqrstuvwx \ EXOSCALE_API_SECRET=xxxxxxx \ -lego --email you@example.com --dns exoscale -d '*.example.com' -d example.com run +lego --dns exoscale -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/f5xc/f5xc.go b/providers/dns/f5xc/f5xc.go index 6f8a8c493..76a6e0262 100644 --- a/providers/dns/f5xc/f5xc.go +++ b/providers/dns/f5xc/f5xc.go @@ -22,6 +22,7 @@ const ( EnvToken = envNamespace + "API_TOKEN" EnvTenantName = envNamespace + "TENANT_NAME" + EnvServer = envNamespace + "SERVER" EnvGroupName = envNamespace + "GROUP_NAME" EnvTTL = envNamespace + "TTL" @@ -34,6 +35,7 @@ const ( type Config struct { APIToken string TenantName string + Server string GroupName string PropagationTimeout time.Duration @@ -71,6 +73,7 @@ func NewDNSProvider() (*DNSProvider, error) { config.APIToken = values[EnvToken] config.TenantName = values[EnvTenantName] config.GroupName = values[EnvGroupName] + config.Server = env.GetOrFile(EnvServer) return NewDNSProviderConfig(config) } @@ -85,7 +88,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("f5xc: missing group name") } - client, err := internal.NewClient(config.APIToken, config.TenantName) + client, err := internal.NewClient(config.APIToken, config.TenantName, config.Server) if err != nil { return nil, fmt.Errorf("f5xc: %w", err) } diff --git a/providers/dns/f5xc/f5xc.toml b/providers/dns/f5xc/f5xc.toml index 7a4cab419..6be604ddd 100644 --- a/providers/dns/f5xc/f5xc.toml +++ b/providers/dns/f5xc/f5xc.toml @@ -8,7 +8,7 @@ Example = ''' F5XC_API_TOKEN="xxx" \ F5XC_TENANT_NAME="yyy" \ F5XC_GROUP_NAME="zzz" \ -lego --email you@example.com --dns f5xc -d '*.example.com' -d example.com run +lego --dns f5xc -d '*.example.com' -d example.com run ''' [Configuration] @@ -17,6 +17,7 @@ lego --email you@example.com --dns f5xc -d '*.example.com' -d example.com run F5XC_TENANT_NAME = "XC Tenant shortname" F5XC_GROUP_NAME = "Group name" [Configuration.Additional] + F5XC_SERVER = "Server domain (Default: console.ves.volterra.io)" F5XC_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" F5XC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" F5XC_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" diff --git a/providers/dns/f5xc/f5xc_test.go b/providers/dns/f5xc/f5xc_test.go index 98f7484e7..890a4cf09 100644 --- a/providers/dns/f5xc/f5xc_test.go +++ b/providers/dns/f5xc/f5xc_test.go @@ -9,7 +9,12 @@ import ( const envDomain = envNamespace + "DOMAIN" -var envTest = tester.NewEnvTest(EnvToken, EnvTenantName, EnvGroupName).WithDomain(envDomain) +var envTest = tester.NewEnvTest( + EnvToken, + EnvTenantName, + EnvServer, + EnvGroupName, +).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { diff --git a/providers/dns/f5xc/internal/client.go b/providers/dns/f5xc/internal/client.go index b0b5d0468..7beab0d03 100644 --- a/providers/dns/f5xc/internal/client.go +++ b/providers/dns/f5xc/internal/client.go @@ -14,7 +14,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -const defaultHost = "console.ves.volterra.io" +const defaultServer = "console.ves.volterra.io" const authorizationHeader = "Authorization" @@ -27,18 +27,14 @@ type Client struct { } // NewClient creates a new Client. -func NewClient(apiToken, tenantName string) (*Client, error) { +func NewClient(apiToken, tenantName, server string) (*Client, error) { if apiToken == "" { return nil, errors.New("credentials missing") } - if tenantName == "" { - return nil, errors.New("missing tenant name") - } - - baseURL, err := url.Parse(fmt.Sprintf("https://%s.%s", tenantName, defaultHost)) + baseURL, err := createBaseURL(tenantName, server) if err != nil { - return nil, fmt.Errorf("parse base URL: %w", err) + return nil, err } return &Client{ @@ -209,3 +205,20 @@ func parseError(req *http.Request, resp *http.Response) error { return &apiErr } + +func createBaseURL(tenant, server string) (*url.URL, error) { + if tenant == "" { + return nil, errors.New("missing tenant name") + } + + if server == "" { + server = defaultServer + } + + baseURL, err := url.Parse(fmt.Sprintf("https://%s.%s", tenant, server)) + if err != nil { + return nil, fmt.Errorf("parse base URL: %w", err) + } + + return baseURL, nil +} diff --git a/providers/dns/f5xc/internal/client_test.go b/providers/dns/f5xc/internal/client_test.go index 0357abb16..bb188ef3f 100644 --- a/providers/dns/f5xc/internal/client_test.go +++ b/providers/dns/f5xc/internal/client_test.go @@ -14,7 +14,7 @@ import ( func mockBuilder() *servermock.Builder[*Client] { return servermock.NewBuilder[*Client]( func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret", "shortname") + client, err := NewClient("secret", "shortname", "") if err != nil { return nil, err } @@ -28,7 +28,7 @@ func mockBuilder() *servermock.Builder[*Client] { WithAuthorization("APIToken secret")) } -func TestClient_Create(t *testing.T) { +func TestClient_CreateRRSet(t *testing.T) { client := mockBuilder(). Route("POST /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA", servermock.ResponseFromFixture("create.json"), @@ -62,7 +62,7 @@ func TestClient_Create(t *testing.T) { assert.Equal(t, expected, result) } -func TestClient_Create_error(t *testing.T) { +func TestClient_CreateRRSet_error(t *testing.T) { client := mockBuilder(). Route("POST /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA", servermock.Noop().WithStatusCode(http.StatusBadRequest)). @@ -81,7 +81,7 @@ func TestClient_Create_error(t *testing.T) { require.Error(t, err) } -func TestClient_Get(t *testing.T) { +func TestClient_GetRRSet(t *testing.T) { client := mockBuilder(). Route("GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.ResponseFromFixture("get.json")). @@ -108,7 +108,7 @@ func TestClient_Get(t *testing.T) { assert.Equal(t, expected, result) } -func TestClient_Get_not_found(t *testing.T) { +func TestClient_GetRRSet_not_found(t *testing.T) { client := mockBuilder(). Route("GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.ResponseFromFixture("error_404.json").WithStatusCode(http.StatusNotFound)). @@ -120,7 +120,7 @@ func TestClient_Get_not_found(t *testing.T) { assert.Nil(t, result) } -func TestClient_Get_error(t *testing.T) { +func TestClient_GetRRSet_error(t *testing.T) { client := mockBuilder(). Route("GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.Noop().WithStatusCode(http.StatusBadRequest)). @@ -130,7 +130,7 @@ func TestClient_Get_error(t *testing.T) { require.Error(t, err) } -func TestClient_Delete(t *testing.T) { +func TestClient_DeleteRRSet(t *testing.T) { client := mockBuilder(). Route("DELETE /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.ResponseFromFixture("get.json")). @@ -157,7 +157,7 @@ func TestClient_Delete(t *testing.T) { assert.Equal(t, expected, result) } -func TestClient_Delete_error(t *testing.T) { +func TestClient_DeleteRRSet_error(t *testing.T) { client := mockBuilder(). Route("DELETE /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.Noop().WithStatusCode(http.StatusBadRequest)). @@ -167,7 +167,7 @@ func TestClient_Delete_error(t *testing.T) { require.Error(t, err) } -func TestClient_Replace(t *testing.T) { +func TestClient_ReplaceRRSet(t *testing.T) { client := mockBuilder(). Route("PUT /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.ResponseFromFixture("get.json"), @@ -204,7 +204,7 @@ func TestClient_Replace(t *testing.T) { assert.Equal(t, expected, result) } -func TestClient_Replace_error(t *testing.T) { +func TestClient_ReplaceRRSet_error(t *testing.T) { client := mockBuilder(). Route("PUT /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.Noop().WithStatusCode(http.StatusBadRequest)). @@ -222,3 +222,70 @@ func TestClient_Replace_error(t *testing.T) { _, err := client.ReplaceRRSet(t.Context(), "example.com", "groupA", "www", "TXT", rrSet) require.Error(t, err) } + +func Test_createBaseURL(t *testing.T) { + testCases := []struct { + desc string + tenant string + server string + expected string + }{ + { + desc: "only tenant", + tenant: "foo", + expected: "https://foo.console.ves.volterra.io", + }, + { + desc: "custom server", + tenant: "foo", + server: "example.com", + expected: "https://foo.example.com", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + baseURL, err := createBaseURL(test.tenant, test.server) + require.NoError(t, err) + + assert.Equal(t, test.expected, baseURL.String()) + }) + } +} + +func Test_createBaseURL_error(t *testing.T) { + testCases := []struct { + desc string + tenant string + server string + expected string + }{ + { + desc: "no tenant", + tenant: "", + expected: "missing tenant name", + }, + { + desc: "invalid tenant", + tenant: "%31", + expected: `parse base URL: parse "https://%31.console.ves.volterra.io": invalid URL escape "%31"`, + }, + { + desc: "invalid host", + tenant: "foo", + server: "192.168.0.%31", + expected: `parse base URL: parse "https://foo.192.168.0.%31": invalid URL escape "%31"`, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + _, err := createBaseURL(test.tenant, test.server) + require.EqualError(t, err, test.expected) + }) + } +} diff --git a/providers/dns/freemyip/freemyip.toml b/providers/dns/freemyip/freemyip.toml index 4821e2a9c..adbf9e213 100644 --- a/providers/dns/freemyip/freemyip.toml +++ b/providers/dns/freemyip/freemyip.toml @@ -6,7 +6,7 @@ Since = "v4.5.0" Example = ''' FREEMYIP_TOKEN=xxxxxx \ -lego --email you@example.com --dns freemyip -d '*.example.com' -d example.com run +lego --dns freemyip -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/gandi/gandi.toml b/providers/dns/gandi/gandi.toml index 96d5233be..23d7de5db 100644 --- a/providers/dns/gandi/gandi.toml +++ b/providers/dns/gandi/gandi.toml @@ -6,7 +6,7 @@ Since = "v0.3.0" Example = ''' GANDI_API_KEY=abcdefghijklmnopqrstuvwx \ -lego --email you@example.com --dns gandi -d '*.example.com' -d example.com run +lego --dns gandi -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/gandiv5/gandiv5.toml b/providers/dns/gandiv5/gandiv5.toml index 246b03524..31568e89b 100644 --- a/providers/dns/gandiv5/gandiv5.toml +++ b/providers/dns/gandiv5/gandiv5.toml @@ -6,7 +6,7 @@ Since = "v0.5.0" Example = ''' GANDIV5_PERSONAL_ACCESS_TOKEN=abcdefghijklmnopqrstuvwx \ -lego --email you@example.com --dns gandiv5 -d '*.example.com' -d example.com run +lego --dns gandiv5 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/gandiv5/internal/client.go b/providers/dns/gandiv5/internal/client.go index 36e0dafb1..bfb71c9f6 100644 --- a/providers/dns/gandiv5/internal/client.go +++ b/providers/dns/gandiv5/internal/client.go @@ -17,9 +17,6 @@ import ( // defaultBaseURL endpoint is the Gandi API endpoint used by Present and CleanUp. const defaultBaseURL = "https://api.gandi.net/v5/livedns" -// APIKeyHeader API key header. -const APIKeyHeader = "X-Api-Key" - // Related to Personal Access Token. const authorizationHeader = "Authorization" @@ -133,7 +130,7 @@ func (c *Client) DeleteTXTRecord(ctx context.Context, domain, name string) error func (c *Client) do(req *http.Request, result any) error { if c.apiKey != "" { - req.Header.Set(APIKeyHeader, c.apiKey) + req.Header.Set(authorizationHeader, "Apikey "+c.apiKey) } if c.pat != "" { diff --git a/providers/dns/gandiv5/internal/client_test.go b/providers/dns/gandiv5/internal/client_test.go index 2465566f9..6a4158dcb 100644 --- a/providers/dns/gandiv5/internal/client_test.go +++ b/providers/dns/gandiv5/internal/client_test.go @@ -9,23 +9,29 @@ import ( "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { +func mockBuilder(apiKey, pat string) *servermock.Builder[*Client] { + checkHeaders := servermock.CheckHeader().WithJSONHeaders() + + if apiKey != "" { + checkHeaders = checkHeaders.WithAuthorization("Apikey secret-apikey") + } else { + checkHeaders = checkHeaders.WithAuthorization("Bearer secret-pat") + } + return servermock.NewBuilder[*Client]( func(server *httptest.Server) (*Client, error) { - client := NewClient("secret", "xxx") + client := NewClient(apiKey, pat) client.BaseURL, _ = url.Parse(server.URL) client.HTTPClient = server.Client() return client, nil }, - servermock.CheckHeader().WithJSONHeaders(). - With("X-Api-Key", "secret"). - WithAuthorization("Bearer xxx"), + checkHeaders, ) } func TestClient_AddTXTRecord(t *testing.T) { - client := mockBuilder(). + client := mockBuilder("secret-apikey", ""). Route("GET /domains/example.com/records/foo/TXT", servermock.ResponseFromFixture("add_txt_record_get.json")). Route("PUT /domains/example.com/records/foo/TXT", @@ -38,7 +44,7 @@ func TestClient_AddTXTRecord(t *testing.T) { } func TestClient_DeleteTXTRecord(t *testing.T) { - client := mockBuilder(). + client := mockBuilder("", "secret-pat"). Route("DELETE /domains/example.com/records/foo/TXT", servermock.ResponseFromFixture("api_response.json")). Build(t) diff --git a/providers/dns/gcloud/gcloud.toml b/providers/dns/gcloud/gcloud.toml index 471e2e9d1..63d22bed3 100644 --- a/providers/dns/gcloud/gcloud.toml +++ b/providers/dns/gcloud/gcloud.toml @@ -8,18 +8,18 @@ Example = ''' # Using a service account file GCE_PROJECT="gc-project-id" \ GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \ -lego --email you@example.com --dns gcloud -d '*.example.com' -d example.com run +lego --dns gcloud -d '*.example.com' -d example.com run # Using default credentials with impersonation GCE_PROJECT="gc-project-id" \ GCE_IMPERSONATE_SERVICE_ACCOUNT="target-sa@gc-project-id.iam.gserviceaccount.com" \ -lego --email you@example.com --dns gcloud -d '*.example.com' -d example.com run +lego --dns gcloud -d '*.example.com' -d example.com run # Using service account key with impersonation GCE_PROJECT="gc-project-id" \ GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \ GCE_IMPERSONATE_SERVICE_ACCOUNT="target-sa@gc-project-id.iam.gserviceaccount.com" \ -lego --email you@example.com --dns gcloud -d '*.example.com' -d example.com run +lego --dns gcloud -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/gcore/gcore.toml b/providers/dns/gcore/gcore.toml index 986455e80..983c35f8a 100644 --- a/providers/dns/gcore/gcore.toml +++ b/providers/dns/gcore/gcore.toml @@ -6,7 +6,7 @@ Since = "v4.5.0" Example = ''' GCORE_PERMANENT_API_TOKEN=xxxxx \ -lego --email you@example.com --dns gcore -d '*.example.com' -d example.com run +lego --dns gcore -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/gigahostno/gigahostno.toml b/providers/dns/gigahostno/gigahostno.toml index 689b96569..b8d3fad2b 100644 --- a/providers/dns/gigahostno/gigahostno.toml +++ b/providers/dns/gigahostno/gigahostno.toml @@ -7,7 +7,7 @@ Since = "v4.29.0" Example = ''' GIGAHOSTNO_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ GIGAHOSTNO_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ -lego --email you@example.com --dns gigahostno -d '*.example.com' -d example.com run +lego --dns gigahostno -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/gigahostno/internal/client_test.go b/providers/dns/gigahostno/internal/client_test.go index aac65bceb..8d1298947 100644 --- a/providers/dns/gigahostno/internal/client_test.go +++ b/providers/dns/gigahostno/internal/client_test.go @@ -38,55 +38,25 @@ func TestClient_GetZones(t *testing.T) { expected := []Zone{ { - ID: "123", - Name: "example.com", - NameDisplay: "example.com", - Type: "NATIVE", - Active: "1", - Protected: "1", - IsRegistered: "1", - Updated: false, - CustomerID: "16030", - DomainRegistrar: "norid", - DomainStatus: "active", - DomainExpiryDate: "2026-11-23 15:17:38", - DomainAutoRenew: "1", - ExternalDNS: "0", - RecordCount: 4, + ID: "123", + Name: "example.com", + NameDisplay: "example.com", + Type: "NATIVE", + Active: "1", }, { - ID: "226", - Name: "example.org", - NameDisplay: "example.org", - Type: "NATIVE", - Active: "1", - Protected: "1", - IsRegistered: "1", - Updated: false, - CustomerID: "16030", - DomainRegistrar: "norid", - DomainStatus: "active", - DomainExpiryDate: "2026-11-23 14:15:01", - DomainAutoRenew: "1", - ExternalDNS: "0", - RecordCount: 5, + ID: "226", + Name: "example.org", + NameDisplay: "example.org", + Type: "NATIVE", + Active: "1", }, { - ID: "229", - Name: "example.xn--zckzah", - NameDisplay: "example.テスト", - Type: "NATIVE", - Active: "1", - Protected: "1", - IsRegistered: "1", - Updated: false, - CustomerID: "16030", - DomainRegistrar: "norid", - DomainStatus: "active", - DomainExpiryDate: "2026-12-01 12:40:48", - DomainAutoRenew: "1", - ExternalDNS: "0", - RecordCount: 4, + ID: "229", + Name: "example.xn--zckzah", + NameDisplay: "example.テスト", + Type: "NATIVE", + Active: "1", }, } diff --git a/providers/dns/gigahostno/internal/fixtures/zones.json b/providers/dns/gigahostno/internal/fixtures/zones.json index f4d927335..d45b0ac49 100644 --- a/providers/dns/gigahostno/internal/fixtures/zones.json +++ b/providers/dns/gigahostno/internal/fixtures/zones.json @@ -30,7 +30,7 @@ "domain_dnssec_data": null, "domain_protected_email": null, "zone_created": "2025-11-23 16:17:29", - "zone_updated": false, + "zone_updated": 1700000000, "external_dns": "0", "record_count": 4, "zone_name_display": "example.com" @@ -59,7 +59,7 @@ "domain_dnssec_data": null, "domain_protected_email": null, "zone_created": "2025-11-23 15:13:27", - "zone_updated": false, + "zone_updated": 1700000000, "external_dns": "0", "record_count": 5, "zone_name_display": "example.org" @@ -88,7 +88,7 @@ "domain_dnssec_data": null, "domain_protected_email": null, "zone_created": "2025-11-23 16:37:15", - "zone_updated": false, + "zone_updated": 1700000000, "external_dns": "0", "record_count": 4, "zone_name_display": "example.\u30C6\u30B9\u30C8" diff --git a/providers/dns/gigahostno/internal/types.go b/providers/dns/gigahostno/internal/types.go index cbb7b8b23..e998dc084 100644 --- a/providers/dns/gigahostno/internal/types.go +++ b/providers/dns/gigahostno/internal/types.go @@ -26,21 +26,11 @@ type APIResponse[T any] struct { } type Zone struct { - ID string `json:"zone_id,omitempty"` - Name string `json:"zone_name,omitempty"` - NameDisplay string `json:"zone_name_display,omitempty"` - Type string `json:"zone_type,omitempty"` - Active string `json:"zone_active,omitempty"` - Protected string `json:"zone_protected,omitempty"` - IsRegistered string `json:"zone_is_registered,omitempty"` - Updated bool `json:"zone_updated,omitempty"` - CustomerID string `json:"cust_id,omitempty"` - DomainRegistrar string `json:"domain_registrar,omitempty"` - DomainStatus string `json:"domain_status,omitempty"` - DomainExpiryDate string `json:"domain_expiry_date,omitempty"` - DomainAutoRenew string `json:"domain_auto_renew,omitempty"` - ExternalDNS string `json:"external_dns,omitempty"` - RecordCount int `json:"record_count,omitempty"` + ID string `json:"zone_id,omitempty"` + Name string `json:"zone_name,omitempty"` + NameDisplay string `json:"zone_name_display,omitempty"` + Type string `json:"zone_type,omitempty"` + Active string `json:"zone_active,omitempty"` } type Record struct { diff --git a/providers/dns/glesys/glesys.toml b/providers/dns/glesys/glesys.toml index 1bdd43c2b..c0e2613b8 100644 --- a/providers/dns/glesys/glesys.toml +++ b/providers/dns/glesys/glesys.toml @@ -7,7 +7,7 @@ Since = "v0.5.0" Example = ''' GLESYS_API_USER=xxxxx \ GLESYS_API_KEY=yyyyy \ -lego --email you@example.com --dns glesys -d '*.example.com' -d example.com run +lego --dns glesys -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/godaddy/godaddy.toml b/providers/dns/godaddy/godaddy.toml index acf0bf404..b906605b3 100644 --- a/providers/dns/godaddy/godaddy.toml +++ b/providers/dns/godaddy/godaddy.toml @@ -7,7 +7,7 @@ Since = "v0.5.0" Example = ''' GODADDY_API_KEY=xxxxxxxx \ GODADDY_API_SECRET=yyyyyyyy \ -lego --email you@example.com --dns godaddy -d '*.example.com' -d example.com run +lego --dns godaddy -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/godaddy/internal/types.go b/providers/dns/godaddy/internal/types.go index c1e6d6638..3bd5c9560 100644 --- a/providers/dns/godaddy/internal/types.go +++ b/providers/dns/godaddy/internal/types.go @@ -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(" ") diff --git a/providers/dns/googledomains/googledomains.toml b/providers/dns/googledomains/googledomains.toml index 1ac7e5e54..52330795d 100644 --- a/providers/dns/googledomains/googledomains.toml +++ b/providers/dns/googledomains/googledomains.toml @@ -8,7 +8,7 @@ Since = "v4.11.0" Example = ''' GOOGLE_DOMAINS_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns googledomains -d '*.example.com' -d example.com run +lego --dns googledomains -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/gravity/gravity.toml b/providers/dns/gravity/gravity.toml index 6010e26e1..87a303839 100644 --- a/providers/dns/gravity/gravity.toml +++ b/providers/dns/gravity/gravity.toml @@ -8,7 +8,7 @@ Example = ''' GRAVITY_SERVER_URL="https://example.org:1234" \ GRAVITY_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ GRAVITY_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ -lego --email you@example.com --dns gravity -d '*.example.com' -d example.com run +lego --dns gravity -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/gravity/internal/types.go b/providers/dns/gravity/internal/types.go index cb6e99083..872bc070f 100644 --- a/providers/dns/gravity/internal/types.go +++ b/providers/dns/gravity/internal/types.go @@ -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) } } diff --git a/providers/dns/hetzner/hetzner.go b/providers/dns/hetzner/hetzner.go index 1b02590d6..bae985b3e 100644 --- a/providers/dns/hetzner/hetzner.go +++ b/providers/dns/hetzner/hetzner.go @@ -4,7 +4,6 @@ package hetzner import ( "errors" "net/http" - "os" "time" "github.com/go-acme/lego/v4/challenge" @@ -62,10 +61,9 @@ type DNSProvider struct { } // NewDNSProvider returns a DNSProvider instance configured for hetzner. -// Credentials must be passed in the environment variable: HETZNER_API_KEY. func NewDNSProvider() (*DNSProvider, error) { - _, foundAPIToken := os.LookupEnv(EnvAPIToken) - _, foundAPIKey := os.LookupEnv(EnvAPIKey) + foundAPIToken := env.GetOrFile(EnvAPIToken) != "" + foundAPIKey := env.GetOrFile(EnvAPIKey) != "" switch { case foundAPIToken: diff --git a/providers/dns/hetzner/hetzner.toml b/providers/dns/hetzner/hetzner.toml index ee1f9a970..40d4cea72 100644 --- a/providers/dns/hetzner/hetzner.toml +++ b/providers/dns/hetzner/hetzner.toml @@ -6,7 +6,7 @@ Since = "v3.7.0" Example = ''' HETZNER_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns hetzner -d '*.example.com' -d example.com run +lego --dns hetzner -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/types.go b/providers/dns/hetzner/internal/hetznerv1/internal/types.go index 47e8a3f91..2b38a8a8c 100644 --- a/providers/dns/hetzner/internal/hetznerv1/internal/types.go +++ b/providers/dns/hetzner/internal/hetznerv1/internal/types.go @@ -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() diff --git a/providers/dns/hostingde/hostingde.toml b/providers/dns/hostingde/hostingde.toml index 569e8a781..502a7fe9e 100644 --- a/providers/dns/hostingde/hostingde.toml +++ b/providers/dns/hostingde/hostingde.toml @@ -6,7 +6,7 @@ Since = "v1.1.0" Example = ''' HOSTINGDE_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns hostingde -d '*.example.com' -d example.com run +lego --dns hostingde -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/hostinger/hostinger.toml b/providers/dns/hostinger/hostinger.toml index f49e447ed..a6f152e73 100644 --- a/providers/dns/hostinger/hostinger.toml +++ b/providers/dns/hostinger/hostinger.toml @@ -6,7 +6,7 @@ Since = "v4.27.0" Example = ''' HOSTINGER_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns hostinger -d '*.example.com' -d example.com run +lego --dns hostinger -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/hostinger/internal/types.go b/providers/dns/hostinger/internal/types.go index 534dfa5e5..c1a02ff8c 100644 --- a/providers/dns/hostinger/internal/types.go +++ b/providers/dns/hostinger/internal/types.go @@ -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() diff --git a/providers/dns/hostingnl/hostingnl.toml b/providers/dns/hostingnl/hostingnl.toml index a26c07ab2..943264ed3 100644 --- a/providers/dns/hostingnl/hostingnl.toml +++ b/providers/dns/hostingnl/hostingnl.toml @@ -6,7 +6,7 @@ Since = "v4.30.0" Example = ''' HOSTINGNL_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns hostingnl -d '*.example.com' -d example.com run +lego --dns hostingnl -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/hosttech/hosttech.toml b/providers/dns/hosttech/hosttech.toml index 5d7555499..52c01fd31 100644 --- a/providers/dns/hosttech/hosttech.toml +++ b/providers/dns/hosttech/hosttech.toml @@ -6,7 +6,7 @@ Since = "v4.5.0" Example = ''' HOSTTECH_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns hosttech -d '*.example.com' -d example.com run +lego --dns hosttech -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/hosttech/internal/types.go b/providers/dns/hosttech/internal/types.go index 854fc4883..a4b5b564d 100644 --- a/providers/dns/hosttech/internal/types.go +++ b/providers/dns/hosttech/internal/types.go @@ -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() diff --git a/providers/dns/httpnet/httpnet.toml b/providers/dns/httpnet/httpnet.toml index 204f5bc54..3dd581204 100644 --- a/providers/dns/httpnet/httpnet.toml +++ b/providers/dns/httpnet/httpnet.toml @@ -6,7 +6,7 @@ Since = "v4.15.0" Example = ''' HTTPNET_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns httpnet -d '*.example.com' -d example.com run +lego --dns httpnet -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/httpreq/httpreq.toml b/providers/dns/httpreq/httpreq.toml index 6c3f8719b..d64d61a6c 100644 --- a/providers/dns/httpreq/httpreq.toml +++ b/providers/dns/httpreq/httpreq.toml @@ -6,7 +6,7 @@ Since = "v2.0.0" Example = ''' HTTPREQ_ENDPOINT=http://my.server.com:9090 \ -lego --email you@example.com --dns httpreq -d '*.example.com' -d example.com run +lego --dns httpreq -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/huaweicloud/huaweicloud.toml b/providers/dns/huaweicloud/huaweicloud.toml index f7991dfae..e8d417c11 100644 --- a/providers/dns/huaweicloud/huaweicloud.toml +++ b/providers/dns/huaweicloud/huaweicloud.toml @@ -8,7 +8,7 @@ Example = ''' HUAWEICLOUD_ACCESS_KEY_ID=your-access-key-id \ HUAWEICLOUD_SECRET_ACCESS_KEY=your-secret-access-key \ HUAWEICLOUD_REGION=cn-south-1 \ -lego --email you@example.com --dns huaweicloud -d '*.example.com' -d example.com run +lego --dns huaweicloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/hurricane/hurricane.toml b/providers/dns/hurricane/hurricane.toml index 033c73984..10b370e4f 100644 --- a/providers/dns/hurricane/hurricane.toml +++ b/providers/dns/hurricane/hurricane.toml @@ -6,10 +6,10 @@ Since = "v4.3.0" Example = ''' HURRICANE_TOKENS=example.org:token \ -lego --email you@example.com --dns hurricane -d '*.example.com' -d example.com run +lego --dns hurricane -d '*.example.com' -d example.com run HURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 \ -lego --email you@example.com --dns hurricane -d my.example.org -d demo.example.org +lego --dns hurricane -d my.example.org -d demo.example.org ''' Additional = """ diff --git a/providers/dns/hyperone/hyperone.toml b/providers/dns/hyperone/hyperone.toml index 0f23976c4..88814356f 100644 --- a/providers/dns/hyperone/hyperone.toml +++ b/providers/dns/hyperone/hyperone.toml @@ -5,7 +5,7 @@ Code = "hyperone" Since = "v3.9.0" Example = ''' -lego --email you@example.com --dns hyperone -d '*.example.com' -d example.com run +lego --dns hyperone -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/ibmcloud/ibmcloud.toml b/providers/dns/ibmcloud/ibmcloud.toml index 2a6c12f82..01088f09b 100644 --- a/providers/dns/ibmcloud/ibmcloud.toml +++ b/providers/dns/ibmcloud/ibmcloud.toml @@ -7,7 +7,7 @@ Since = "v4.5.0" Example = ''' SOFTLAYER_USERNAME=xxxxx \ SOFTLAYER_API_KEY=yyyyy \ -lego --email you@example.com --dns ibmcloud -d '*.example.com' -d example.com run +lego --dns ibmcloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/iij/iij.toml b/providers/dns/iij/iij.toml index 8dbf5ba1a..95355200a 100644 --- a/providers/dns/iij/iij.toml +++ b/providers/dns/iij/iij.toml @@ -8,7 +8,7 @@ Example = ''' IIJ_API_ACCESS_KEY=xxxxxxxx \ IIJ_API_SECRET_KEY=yyyyyy \ IIJ_DO_SERVICE_CODE=zzzzzz \ -lego --email you@example.com --dns iij -d '*.example.com' -d example.com run +lego --dns iij -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/iijdpf/iijdpf.toml b/providers/dns/iijdpf/iijdpf.toml index 4aaa9ca37..650285f95 100644 --- a/providers/dns/iijdpf/iijdpf.toml +++ b/providers/dns/iijdpf/iijdpf.toml @@ -7,7 +7,7 @@ Since = "v4.7.0" Example = ''' IIJ_DPF_API_TOKEN=xxxxxxxx \ IIJ_DPF_DPM_SERVICE_CODE=yyyyyy \ -lego --email you@example.com --dns iijdpf -d '*.example.com' -d example.com run +lego --dns iijdpf -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/infoblox/infoblox.toml b/providers/dns/infoblox/infoblox.toml index 3c2632042..0e6972d3a 100644 --- a/providers/dns/infoblox/infoblox.toml +++ b/providers/dns/infoblox/infoblox.toml @@ -8,7 +8,7 @@ Example = ''' INFOBLOX_USERNAME=api-user-529 \ INFOBLOX_PASSWORD=b9841238feb177a84330febba8a83208921177bffe733 \ INFOBLOX_HOST=infoblox.example.org -lego --email you@example.com --dns infoblox -d '*.example.com' -d example.com run +lego --dns infoblox -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/infomaniak/infomaniak.toml b/providers/dns/infomaniak/infomaniak.toml index 283838053..d924e3a26 100644 --- a/providers/dns/infomaniak/infomaniak.toml +++ b/providers/dns/infomaniak/infomaniak.toml @@ -6,7 +6,7 @@ Since = "v4.1.0" Example = ''' INFOMANIAK_ACCESS_TOKEN=1234567898765432 \ -lego --email you@example.com --dns infomaniak -d '*.example.com' -d example.com run +lego --dns infomaniak -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/internal/selectel/internal/client.go b/providers/dns/internal/selectel/internal/client.go index b17df6d83..d441c9894 100644 --- a/providers/dns/internal/selectel/internal/client.go +++ b/providers/dns/internal/selectel/internal/client.go @@ -53,8 +53,8 @@ func (c *Client) GetDomainByName(ctx context.Context, domainName string) (*Domai if err != nil { if statusCode == http.StatusNotFound && strings.Count(domainName, ".") > 1 { // Look up for the next subdomain - subIndex := strings.Index(domainName, ".") - return c.GetDomainByName(ctx, domainName[subIndex+1:]) + _, after, _ := strings.Cut(domainName, ".") + return c.GetDomainByName(ctx, after) } return nil, err diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index 3db3877ef..090c9109a 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -10,12 +10,12 @@ import ( const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "goacme-lego/4.30.1" + 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 // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) // Get builds and returns the User-Agent string. diff --git a/providers/dns/westcn/internal/client.go b/providers/dns/internal/westcn/internal/client.go similarity index 98% rename from providers/dns/westcn/internal/client.go rename to providers/dns/internal/westcn/internal/client.go index bfed159ae..621c7865f 100644 --- a/providers/dns/westcn/internal/client.go +++ b/providers/dns/internal/westcn/internal/client.go @@ -30,7 +30,7 @@ type Client struct { encoder *encoding.Encoder - baseURL *url.URL + BaseURL *url.URL HTTPClient *http.Client } @@ -46,7 +46,7 @@ func NewClient(username, password string) (*Client, error) { username: username, password: password, encoder: simplifiedchinese.GBK.NewEncoder(), - baseURL: baseURL, + BaseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, }, nil } @@ -116,7 +116,7 @@ func (c *Client) newRequest(ctx context.Context, p, act string, form url.Values) return nil, err } - endpoint := c.baseURL.JoinPath(p, "/") + endpoint := c.BaseURL.JoinPath(p, "/") query := endpoint.Query() query.Set("act", act) diff --git a/providers/dns/westcn/internal/client_test.go b/providers/dns/internal/westcn/internal/client_test.go similarity index 98% rename from providers/dns/westcn/internal/client_test.go rename to providers/dns/internal/westcn/internal/client_test.go index f7bdac5c0..53fd6ed8f 100644 --- a/providers/dns/westcn/internal/client_test.go +++ b/providers/dns/internal/westcn/internal/client_test.go @@ -21,7 +21,7 @@ func mockBuilder() *servermock.Builder[*Client] { } client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) + client.BaseURL, _ = url.Parse(server.URL) return client, nil }, @@ -69,7 +69,8 @@ func TestClientAddRecord_error(t *testing.T) { servermock.ResponseFromFixture("error.json"). WithHeader("Content-Type", "application/json", "Charset=gb2312"), servermock.CheckQueryParameter().Strict(). - With("act", "adddnsrecord")). + With("act", "adddnsrecord"), + ). Build(t) record := Record{ diff --git a/providers/dns/westcn/internal/fixtures/adddnsrecord.json b/providers/dns/internal/westcn/internal/fixtures/adddnsrecord.json similarity index 100% rename from providers/dns/westcn/internal/fixtures/adddnsrecord.json rename to providers/dns/internal/westcn/internal/fixtures/adddnsrecord.json diff --git a/providers/dns/westcn/internal/fixtures/deldnsrecord.json b/providers/dns/internal/westcn/internal/fixtures/deldnsrecord.json similarity index 100% rename from providers/dns/westcn/internal/fixtures/deldnsrecord.json rename to providers/dns/internal/westcn/internal/fixtures/deldnsrecord.json diff --git a/providers/dns/westcn/internal/fixtures/error.json b/providers/dns/internal/westcn/internal/fixtures/error.json similarity index 100% rename from providers/dns/westcn/internal/fixtures/error.json rename to providers/dns/internal/westcn/internal/fixtures/error.json diff --git a/providers/dns/westcn/internal/types.go b/providers/dns/internal/westcn/internal/types.go similarity index 100% rename from providers/dns/westcn/internal/types.go rename to providers/dns/internal/westcn/internal/types.go diff --git a/providers/dns/internal/westcn/provider.go b/providers/dns/internal/westcn/provider.go new file mode 100644 index 000000000..a9e6dad58 --- /dev/null +++ b/providers/dns/internal/westcn/provider.go @@ -0,0 +1,140 @@ +package westcn + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "sync" + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" + "github.com/go-acme/lego/v4/providers/dns/internal/westcn/internal" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Username string + Password string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client + + recordIDs map[string]int + recordIDsMu sync.Mutex +} + +// NewDNSProviderConfig return a DNSProvider instance configured for West.cn/西部数码. +func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.Username, config.Password) + if err != nil { + return nil, fmt.Errorf("%w", err) + } + + if baseURL != "" { + client.BaseURL, err = url.Parse(baseURL) + if err != nil { + return nil, err + } + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]int), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("%w", err) + } + + record := internal.Record{ + Domain: dns01.UnFqdn(authZone), + Host: subDomain, + Type: "TXT", + Value: info.Value, + TTL: d.config.TTL, + } + + recordID, err := d.client.AddRecord(context.Background(), record) + if err != nil { + return fmt.Errorf("add record: %w", err) + } + + d.recordIDsMu.Lock() + d.recordIDs[token] = recordID + d.recordIDsMu.Unlock() + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("could not find zone for domain %q: %w", domain, err) + } + + // gets the record's unique ID + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + + if !ok { + return fmt.Errorf("unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID) + if err != nil { + return fmt.Errorf("delete record: %w", err) + } + + // deletes record ID from map + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/dns/internal/westcn/provider_test.go b/providers/dns/internal/westcn/provider_test.go new file mode 100644 index 000000000..2ae0f09cb --- /dev/null +++ b/providers/dns/internal/westcn/provider_test.go @@ -0,0 +1,127 @@ +package westcn + +import ( + "net/http/httptest" + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +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: "credentials missing", + }, + { + desc: "missing password", + username: "user", + expected: "credentials missing", + }, + { + desc: "missing credentials", + expected: "credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := &Config{} + 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 mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := &Config{ + Username: "user", + Password: "secret", + PropagationTimeout: 10 * time.Second, + PollingInterval: 1 * time.Second, + TTL: 120, + HTTPClient: server.Client(), + } + + p, err := NewDNSProviderConfig(config, server.URL) + if err != nil { + return nil, err + } + + return p, nil + }, + servermock.CheckHeader(). + WithContentTypeFromURLEncoded()) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /domain/", + servermock.ResponseFromInternal("adddnsrecord.json"). + WithHeader("Content-Type", "application/json", "Charset=gb2312"), + servermock.CheckQueryParameter().Strict(). + With("act", "adddnsrecord"), + servermock.CheckForm().UsePostForm().Strict(). + With("domain", "example.com"). + With("host", "_acme-challenge"). + With("ttl", "120"). + With("type", "TXT"). + With("value", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + // With("act", "adddnsrecord"). + With("username", "user"). + WithRegexp("time", `\d+`). + WithRegexp("token", `[a-z0-9]{32}`), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("POST /domain/", + servermock.ResponseFromInternal("deldnsrecord.json"). + WithHeader("Content-Type", "application/json", "Charset=gb2312"), + servermock.CheckQueryParameter().Strict(). + With("act", "deldnsrecord"), + servermock.CheckForm().UsePostForm().Strict(). + With("id", "123"). + With("domain", "example.com"). + With("username", "user"). + WithRegexp("time", `\d+`). + WithRegexp("token", `[a-z0-9]{32}`), + ). + Build(t) + + provider.recordIDs["abc"] = 123 + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/internetbs/internetbs.toml b/providers/dns/internetbs/internetbs.toml index d25418f22..f22850253 100644 --- a/providers/dns/internetbs/internetbs.toml +++ b/providers/dns/internetbs/internetbs.toml @@ -7,7 +7,7 @@ Since = "v4.5.0" Example = ''' INTERNET_BS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ INTERNET_BS_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ -lego --email you@example.com --dns internetbs -d '*.example.com' -d example.com run +lego --dns internetbs -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/inwx/inwx.toml b/providers/dns/inwx/inwx.toml index aeab5a242..da4c6d959 100644 --- a/providers/dns/inwx/inwx.toml +++ b/providers/dns/inwx/inwx.toml @@ -7,13 +7,13 @@ Since = "v2.0.0" Example = ''' INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ -lego --email you@example.com --dns inwx -d '*.example.com' -d example.com run +lego --dns inwx -d '*.example.com' -d example.com run # 2FA INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ INWX_SHARED_SECRET=zzzzzzzzzz \ -lego --email you@example.com --dns inwx -d '*.example.com' -d example.com run +lego --dns inwx -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ionos/ionos.toml b/providers/dns/ionos/ionos.toml index 0c905273f..a2c9518fb 100644 --- a/providers/dns/ionos/ionos.toml +++ b/providers/dns/ionos/ionos.toml @@ -6,7 +6,7 @@ Since = "v4.2.0" Example = ''' IONOS_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns ionos -d '*.example.com' -d example.com run +lego --dns ionos -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ionoscloud/ionoscloud.toml b/providers/dns/ionoscloud/ionoscloud.toml index a8fedce6c..6e1d080e4 100644 --- a/providers/dns/ionoscloud/ionoscloud.toml +++ b/providers/dns/ionoscloud/ionoscloud.toml @@ -6,7 +6,7 @@ Since = "v4.30.0" Example = ''' IONOSCLOUD_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns ionoscloud -d '*.example.com' -d example.com run +lego --dns ionoscloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ipv64/ipv64.toml b/providers/dns/ipv64/ipv64.toml index fba210bdb..aa1720c9e 100644 --- a/providers/dns/ipv64/ipv64.toml +++ b/providers/dns/ipv64/ipv64.toml @@ -6,7 +6,7 @@ Since = "v4.13.0" Example = ''' IPV64_API_KEY=xxxxxx \ -lego --email you@example.com --dns ipv64 -d '*.example.com' -d example.com run +lego --dns ipv64 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ispconfig/internal/client.go b/providers/dns/ispconfig/internal/client.go new file mode 100644 index 000000000..9280fdec1 --- /dev/null +++ b/providers/dns/ispconfig/internal/client.go @@ -0,0 +1,318 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" +) + +type Client struct { + serverURL string + HTTPClient *http.Client +} + +func NewClient(serverURL string) (*Client, error) { + _, err := url.Parse(serverURL) + if err != nil { + return nil, fmt.Errorf("server URL: %w", err) + } + + return &Client{ + serverURL: serverURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) Login(ctx context.Context, username, password string) (string, error) { + payload := LoginRequest{ + Username: username, + Password: password, + ClientLogin: false, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return "", err + } + + endpoint.RawQuery = "login" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return "", err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return "", err + } + + return extractResponse[string](response) +} + +func (c *Client) GetClientID(ctx context.Context, sessionID, sysUserID string) (int, error) { + payload := ClientIDRequest{ + SessionID: sessionID, + SysUserID: sysUserID, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return 0, err + } + + endpoint.RawQuery = "client_get_id" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return 0, err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return 0, err + } + + return extractResponse[int](response) +} + +// GetZoneID returns the zone ID for the given name. +func (c *Client) GetZoneID(ctx context.Context, sessionID, name string) (int, error) { + payload := map[string]any{ + "session_id": sessionID, + "origin": name, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return 0, err + } + + endpoint.RawQuery = "dns_zone_get_id" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return 0, err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return 0, err + } + + return extractResponse[int](response) +} + +// GetZone returns the zone information for the zone ID. +func (c *Client) GetZone(ctx context.Context, sessionID, zoneID string) (*Zone, error) { + payload := map[string]any{ + "session_id": sessionID, + "primary_id": zoneID, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return nil, err + } + + endpoint.RawQuery = "dns_zone_get" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return nil, err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return nil, err + } + + return extractResponse[*Zone](response) +} + +// GetTXT returns the TXT record for the given name. +// `name` must be a fully qualified domain name, e.g. "example.com.". +func (c *Client) GetTXT(ctx context.Context, sessionID, name string) (*Record, error) { + payload := GetTXTRequest{ + SessionID: sessionID, + PrimaryID: struct { + Name string `json:"name"` + Type string `json:"type"` + }{ + Name: name, + Type: "txt", + }, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return nil, err + } + + endpoint.RawQuery = "dns_txt_get" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return nil, err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return nil, err + } + + return extractResponse[*Record](response) +} + +// AddTXT adds a TXT record. +// It returns the ID of the newly created record. +func (c *Client) AddTXT(ctx context.Context, sessionID, clientID string, params RecordParams) (string, error) { + payload := AddTXTRequest{ + SessionID: sessionID, + ClientID: clientID, + Params: ¶ms, + UpdateSerial: true, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return "", err + } + + endpoint.RawQuery = "dns_txt_add" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return "", err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return "", err + } + + return extractResponse[string](response) +} + +// DeleteTXT deletes a TXT record. +// It returns the number of deleted records. +func (c *Client) DeleteTXT(ctx context.Context, sessionID, recordID string) (int, error) { + payload := DeleteTXTRequest{ + SessionID: sessionID, + PrimaryID: recordID, + UpdateSerial: true, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return 0, err + } + + endpoint.RawQuery = "dns_txt_delete" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return 0, err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return 0, err + } + + return extractResponse[int](response) +} + +func (c *Client) do(req *http.Request, result any) error { + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, 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, http.MethodPost, 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 extractResponse[T any](response APIResponse) (T, error) { + if response.Code != "ok" { + var zero T + + return zero, &APIError{APIResponse: response} + } + + var result T + + err := json.Unmarshal(response.Response, &result) + if err != nil { + var zero T + return zero, fmt.Errorf("unable to unmarshal response: %s, %w", string(response.Response), err) + } + + return result, nil +} diff --git a/providers/dns/ispconfig/internal/client_test.go b/providers/dns/ispconfig/internal/client_test.go new file mode 100644 index 000000000..a4db3d5f7 --- /dev/null +++ b/providers/dns/ispconfig/internal/client_test.go @@ -0,0 +1,175 @@ +package internal + +import ( + "net/http/httptest" + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(server.URL) + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + + return client, nil + }) +} + +func TestClient_Login(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("login.json"), + servermock.CheckRequestJSONBodyFromFixture("login-request.json"), + servermock.CheckQueryParameter().Strict(). + With("login", ""), + ). + Build(t) + + sessionID, err := client.Login(t.Context(), "user", "secret") + require.NoError(t, err) + + assert.Equal(t, "abc", sessionID) +} + +func TestClient_Login_error(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("error.json"), + ). + Build(t) + + _, err := client.Login(t.Context(), "user", "secret") + require.EqualError(t, err, `code: remote_fault, message: The login failed. Username or password wrong., response: false`) +} + +func TestClient_GetClientID(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("client_get_id.json"), + servermock.CheckRequestJSONBodyFromFixture("client_get_id-request.json"), + servermock.CheckQueryParameter().Strict(). + With("client_get_id", ""), + ). + Build(t) + + id, err := client.GetClientID(t.Context(), "sessionA", "sysA") + require.NoError(t, err) + + assert.Equal(t, 123, id) +} + +func TestClient_GetZoneID(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("dns_zone_get_id.json"), + servermock.CheckRequestJSONBodyFromFixture("dns_zone_get_id-request.json"), + servermock.CheckQueryParameter().Strict(). + With("dns_zone_get_id", ""), + ). + Build(t) + + zoneID, err := client.GetZoneID(t.Context(), "sessionA", "example.com") + require.NoError(t, err) + + assert.Equal(t, 123, zoneID) +} + +func TestClient_GetZone(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("dns_zone_get.json"), + servermock.CheckRequestJSONBodyFromFixture("dns_zone_get-request.json"), + servermock.CheckQueryParameter().Strict(). + With("dns_zone_get", ""), + ). + Build(t) + + zone, err := client.GetZone(t.Context(), "sessionA", "example.com.") + require.NoError(t, err) + + expected := &Zone{ + ID: "456", + ServerID: "123", + SysUserID: "789", + SysGroupID: "2", + Origin: "example.com.", + Serial: "2025102902", + Active: "Y", + } + + assert.Equal(t, expected, zone) +} + +func TestClient_GetTXT(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("dns_txt_get.json"), + servermock.CheckRequestJSONBodyFromFixture("dns_txt_get-request.json"), + servermock.CheckQueryParameter().Strict(). + With("dns_txt_get", ""), + ). + Build(t) + + record, err := client.GetTXT(t.Context(), "sessionA", "example.com.") + require.NoError(t, err) + + expected := &Record{ID: 123} + + assert.Equal(t, expected, record) +} + +func TestClient_AddTXT(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("dns_txt_add.json"), + servermock.CheckRequestJSONBodyFromFixture("dns_txt_add-request.json"), + servermock.CheckQueryParameter().Strict(). + With("dns_txt_add", ""), + ). + Build(t) + + now := time.Date(2025, 12, 25, 1, 1, 1, 0, time.UTC) + + params := RecordParams{ + ServerID: "serverA", + Zone: "example.com.", + Name: "foo.example.com.", + Type: "txt", + Data: "txtTXTtxt", + Aux: "0", + TTL: "3600", + Active: "y", + Stamp: now.Format("2006-01-02 15:04:05"), + UpdateSerial: true, + } + + recordID, err := client.AddTXT(t.Context(), "sessionA", "clientA", params) + require.NoError(t, err) + + assert.Equal(t, "123", recordID) +} + +func TestClient_DeleteTXT(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("dns_txt_delete.json"), + servermock.CheckRequestJSONBodyFromFixture("dns_txt_delete-request.json"), + servermock.CheckQueryParameter().Strict(). + With("dns_txt_delete", ""), + ). + Build(t) + + count, err := client.DeleteTXT(t.Context(), "sessionA", "123") + require.NoError(t, err) + + assert.Equal(t, 1, count) +} diff --git a/providers/dns/ispconfig/internal/fixtures/client_get_id-request.json b/providers/dns/ispconfig/internal/fixtures/client_get_id-request.json new file mode 100644 index 000000000..ba573f824 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/client_get_id-request.json @@ -0,0 +1,4 @@ +{ + "session_id": "sessionA", + "sys_userid": "sysA" +} diff --git a/providers/dns/ispconfig/internal/fixtures/client_get_id.json b/providers/dns/ispconfig/internal/fixtures/client_get_id.json new file mode 100644 index 000000000..7b9f667a0 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/client_get_id.json @@ -0,0 +1,5 @@ +{ + "code": "ok", + "message": "foo", + "response": 123 +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_add-request.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_add-request.json new file mode 100644 index 000000000..bf5242cd1 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_txt_add-request.json @@ -0,0 +1,17 @@ +{ + "session_id": "sessionA", + "client_id": "clientA", + "params": { + "server_id": "serverA", + "zone": "example.com.", + "name": "foo.example.com.", + "type": "txt", + "data": "txtTXTtxt", + "aux": "0", + "ttl": "3600", + "active": "y", + "stamp": "2025-12-25 01:01:01", + "update_serial": true + }, + "update_serial": true +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_add.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_add.json new file mode 100644 index 000000000..7980619fe --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_txt_add.json @@ -0,0 +1,5 @@ +{ + "code": "ok", + "message": "foo", + "response": "123" +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_delete-request.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_delete-request.json new file mode 100644 index 000000000..240976654 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_txt_delete-request.json @@ -0,0 +1,5 @@ +{ + "session_id": "sessionA", + "primary_id": "123", + "update_serial": true +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_delete.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_delete.json new file mode 100644 index 000000000..960b650bd --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_txt_delete.json @@ -0,0 +1,5 @@ +{ + "code": "ok", + "message": "foo", + "response": 1 +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_get-request.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_get-request.json new file mode 100644 index 000000000..8bda44067 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_txt_get-request.json @@ -0,0 +1,7 @@ +{ + "session_id": "sessionA", + "primary_id": { + "name": "example.com.", + "type": "txt" + } +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_get.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_get.json new file mode 100644 index 000000000..f707d50c3 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_txt_get.json @@ -0,0 +1,7 @@ +{ + "code": "ok", + "message": "foo", + "response": { + "id": 123 + } +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_zone_get-request.json b/providers/dns/ispconfig/internal/fixtures/dns_zone_get-request.json new file mode 100644 index 000000000..3d44d468f --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_zone_get-request.json @@ -0,0 +1,4 @@ +{ + "primary_id": "example.com.", + "session_id": "sessionA" +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_zone_get.json b/providers/dns/ispconfig/internal/fixtures/dns_zone_get.json new file mode 100644 index 000000000..37975d0e6 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_zone_get.json @@ -0,0 +1,32 @@ +{ + "code": "ok", + "message": "foo", + "response": { + "id": "456", + "sys_userid": "789", + "sys_groupid": "2", + "sys_perm_user": "riud", + "sys_perm_group": "riud", + "sys_perm_other": "", + "server_id": "123", + "origin": "example.com.", + "ns": "ns1.example.org.", + "mbox": "support.example.net.", + "serial": "2025102902", + "refresh": "7200", + "retry": "540", + "expire": "604800", + "minimum": "3600", + "ttl": "3600", + "active": "Y", + "xfer": "", + "also_notify": "", + "update_acl": "", + "dnssec_initialized": "N", + "dnssec_wanted": "N", + "dnssec_algo": "ECDSAP256SHA256", + "dnssec_last_signed": "0", + "dnssec_info": "", + "rendered_zone": "" + } +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id-request.json b/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id-request.json new file mode 100644 index 000000000..e3084242e --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id-request.json @@ -0,0 +1,4 @@ +{ + "origin": "example.com", + "session_id": "sessionA" +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id.json b/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id.json new file mode 100644 index 000000000..7b9f667a0 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id.json @@ -0,0 +1,5 @@ +{ + "code": "ok", + "message": "foo", + "response": 123 +} diff --git a/providers/dns/ispconfig/internal/fixtures/error.json b/providers/dns/ispconfig/internal/fixtures/error.json new file mode 100644 index 000000000..a9c76546c --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/error.json @@ -0,0 +1,5 @@ +{ + "code": "remote_fault", + "message": "The login failed. Username or password wrong.", + "response": false +} diff --git a/providers/dns/ispconfig/internal/fixtures/login-request.json b/providers/dns/ispconfig/internal/fixtures/login-request.json new file mode 100644 index 000000000..c3293a2e8 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/login-request.json @@ -0,0 +1,5 @@ +{ + "username": "user", + "password": "secret", + "client_login": false +} diff --git a/providers/dns/ispconfig/internal/fixtures/login.json b/providers/dns/ispconfig/internal/fixtures/login.json new file mode 100644 index 000000000..e380a86ec --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/login.json @@ -0,0 +1,5 @@ +{ + "code": "ok", + "message": "foo", + "response": "abc" +} diff --git a/providers/dns/ispconfig/internal/readme.md b/providers/dns/ispconfig/internal/readme.md new file mode 100644 index 000000000..2284c338f --- /dev/null +++ b/providers/dns/ispconfig/internal/readme.md @@ -0,0 +1,249 @@ +## Error Response + +```json +{ + "code": "", + "message": "", + "response": false +} +``` + +## Login Endpoint + +* URL: `?login` +* HTTP Method: `POST` + +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/login.html +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/login.php + +### Request Body (JSON) + +```json +{ + "username": "", + "password": "", + "client_login": false +} +``` + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": "abc" +} +``` + +- `response`: is the `sessionID` + +## Get Client ID Endpoint + +* URL: `?client_get_id` +* HTTP Method: `POST` + +- function `client_get_id`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/client.inc.php#L97 +- TABLE `sys_user`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L1852 +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/client_get_id.html +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/client_get_id.php + +### Request Body (JSON) + +```json +{ + "session_id": "", + "sys_userid": "" +} +``` + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": 123 +} +``` + +## DNS Zone Get ID Endpoint + +* URL: `?dns_zone_get_id` +* HTTP Method: `POST` + +- function `dns_zone_get_id`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L142 +- TABLE `dns_soa`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L615 + +### Request Body (JSON) + +```json +{ + "session_id": "", + "origin": "" +} +``` + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": 123 +} +``` + +## DNS Zone Get Endpoint + +* URL: `?dns_zone_get` +* HTTP Method: `POST` + +- function `dns_zone_get`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L87 +- function `getDataRecord`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remoting_lib.inc.php#L248 +- TABLE `dns_soa`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L615 +- Depending on the request, the response may be an array or an object (`primary_id` can be a string, an array or an object). +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_zone_get.html +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_zone_get.php + +### Request Body (JSON) + +```json +{ + "session_id": "", + "primary_id": "" +} +``` + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": { + "id": 456, + "server_id": 123, + "sys_userid": 789 + } +} +``` + +## DNS TXT Get Endpoint + +* URL: `?dns_txt_get` +* HTTP Method: `POST` + +- function `dns_txt_get`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L640 +- function `dns_rr_get`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L195 +- form: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/web/dns/form/dns_txt.tform.php +- TABLE `dns_rr`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L490 +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_txt_get.html +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_txt_get.php + +### Request Body (JSON) + +```json +{ + "session_id": "", + "primary_id": { + "name": ".", + "type": "TXT" + } +} +``` + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": { + "id": 123 + } +} +``` + +## DNS TXT Add Endpoint + +* URL: `?dns_txt_add` +* HTTP Method: `POST` + +- function `dns_txt_add`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L645 +- function `dns_rr_add` https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L212 +- form: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/web/dns/form/dns_txt.tform.php +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_txt_add.html +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_txt_add.php + +### Request Body (JSON) + +```json +{ + "session_id": "", + "client_id": "", + "params": { + "server_id": "", + "zone": "", + "name": ".", + "type": "txt", + "data": "", + "aux": "0", + "ttl": "3600", + "active": "y", + "stamp": "", + "update_serial": true + }, + "update_serial": true +} +``` + +- `stamp`: (ex: `2025-12-17 23:35:58`) +- `serial`: (ex: `1766010947`) + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": "123" +} +``` + +## DNS TXT Delete Endpoint + +* URL: `?dns_txt_delete` +* HTTP Method: `POST` + +- function `dns_txt_delete`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L655 +- function `dns_rr_delete`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L247 +- form: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/web/dns/form/dns_txt.tform.php +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_txt_delete.html +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_txt_delete.php + +### Request Body (JSON) + +```json +{ + "session_id": "", + "primary_id": "", + "update_serial": true +} +``` + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": 1 +} +``` + +--- + +https://www.ispconfig.org/ +https://git.ispconfig.org/ispconfig/ispconfig3 +https://forum.howtoforge.com/#ispconfig-3.23 diff --git a/providers/dns/ispconfig/internal/types.go b/providers/dns/ispconfig/internal/types.go new file mode 100644 index 000000000..7db0846cc --- /dev/null +++ b/providers/dns/ispconfig/internal/types.go @@ -0,0 +1,95 @@ +package internal + +import ( + "encoding/json" + "strings" +) + +type APIError struct { + APIResponse +} + +func (e *APIError) Error() string { + var msg strings.Builder + + msg.WriteString("code: " + e.Code) + + if e.Message != "" { + msg.WriteString(", message: " + e.Message) + } + + if len(e.Response) > 0 { + msg.WriteString(", response: " + string(e.Response)) + } + + return msg.String() +} + +type APIResponse struct { + Code string `json:"code"` + Message string `json:"message"` + Response json.RawMessage `json:"response"` +} + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` + ClientLogin bool `json:"client_login"` +} + +type ClientIDRequest struct { + SessionID string `json:"session_id"` + SysUserID string `json:"sys_userid"` +} + +type Zone struct { + ID string `json:"id"` + ServerID string `json:"server_id"` + SysUserID string `json:"sys_userid"` + SysGroupID string `json:"sys_groupid"` + Origin string `json:"origin"` + Serial string `json:"serial"` + Active string `json:"active"` +} + +type GetTXTRequest struct { + SessionID string `json:"session_id"` + PrimaryID struct { + Name string `json:"name"` + Type string `json:"type"` + } `json:"primary_id"` +} + +type Record struct { + ID int `json:"id"` +} + +type AddTXTRequest struct { + SessionID string `json:"session_id"` + ClientID string `json:"client_id"` + Params *RecordParams `json:"params,omitempty"` + UpdateSerial bool `json:"update_serial"` +} + +type RecordParams struct { + ServerID string `json:"server_id"` + Zone string `json:"zone"` + Name string `json:"name"` + // 'a','aaaa','alias','cname','hinfo','mx','naptr','ns','ds','ptr','rp','srv','txt' + Type string `json:"type"` + Data string `json:"data"` + // "0" + Aux string `json:"aux"` + TTL string `json:"ttl"` + // 'n','y' + Active string `json:"active"` + // `2025-12-17 23:35:58` + Stamp string `json:"stamp"` + UpdateSerial bool `json:"update_serial"` +} + +type DeleteTXTRequest struct { + SessionID string `json:"session_id"` + PrimaryID string `json:"primary_id"` + UpdateSerial bool `json:"update_serial"` +} diff --git a/providers/dns/ispconfig/ispconfig.go b/providers/dns/ispconfig/ispconfig.go new file mode 100644 index 000000000..9396430b7 --- /dev/null +++ b/providers/dns/ispconfig/ispconfig.go @@ -0,0 +1,220 @@ +// Package ispconfig implements a DNS provider for solving the DNS-01 challenge using ISPConfig. +package ispconfig + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" + "github.com/go-acme/lego/v4/providers/dns/ispconfig/internal" +) + +// Environment variables names. +const ( + envNamespace = "ISPCONFIG_" + + EnvServerURL = envNamespace + "SERVER_URL" + EnvUsername = envNamespace + "USERNAME" + EnvPassword = envNamespace + "PASSWORD" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" + EnvInsecureSkipVerify = envNamespace + "INSECURE_SKIP_VERIFY" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + ServerURL string + Username string + Password string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client + InsecureSkipVerify bool +} + +// 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 + + recordIDs map[string]string + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for ISPConfig. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvServerURL, EnvUsername, EnvPassword) + if err != nil { + return nil, fmt.Errorf("ispconfig: %w", err) + } + + config := NewDefaultConfig() + config.ServerURL = values[EnvServerURL] + config.Username = values[EnvUsername] + config.Password = values[EnvPassword] + config.InsecureSkipVerify = env.GetOrDefaultBool(EnvInsecureSkipVerify, false) + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for ISPConfig. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("ispconfig: the configuration of the DNS provider is nil") + } + + if config.ServerURL == "" { + return nil, errors.New("ispconfig: missing server URL") + } + + if config.Username == "" || config.Password == "" { + return nil, errors.New("ispconfig: credentials missing") + } + + client, err := internal.NewClient(config.ServerURL) + if err != nil { + return nil, fmt.Errorf("ispconfig: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + if config.InsecureSkipVerify { + client.HTTPClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]string), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + sessionID, err := d.client.Login(ctx, d.config.Username, d.config.Password) + if err != nil { + return fmt.Errorf("ispconfig: login: %w", err) + } + + zoneID, err := d.findZone(ctx, sessionID, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("ispconfig: get zone id: %w", err) + } + + zone, err := d.client.GetZone(ctx, sessionID, strconv.Itoa(zoneID)) + if err != nil { + return fmt.Errorf("ispconfig: get zone: %w", err) + } + + clientID, err := d.client.GetClientID(ctx, sessionID, zone.SysUserID) + if err != nil { + return fmt.Errorf("ispconfig: get client id: %w", err) + } + + params := internal.RecordParams{ + ServerID: "serverA", + Zone: zone.ID, + Name: info.EffectiveFQDN, + Type: "txt", + Data: info.Value, + Aux: "0", + TTL: strconv.Itoa(d.config.TTL), + Active: "y", + Stamp: time.Now().UTC().Format("2006-01-02 15:04:05"), + } + + recordID, err := d.client.AddTXT(ctx, sessionID, strconv.Itoa(clientID), params) + if err != nil { + return fmt.Errorf("ispconfig: add txt record: %w", err) + } + + d.recordIDsMu.Lock() + d.recordIDs[token] = recordID + d.recordIDsMu.Unlock() + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + // gets the record's unique ID + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + + if !ok { + return fmt.Errorf("ispconfig: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + sessionID, err := d.client.Login(ctx, d.config.Username, d.config.Password) + if err != nil { + return fmt.Errorf("ispconfig: login: %w", err) + } + + _, err = d.client.DeleteTXT(ctx, sessionID, recordID) + if err != nil { + return fmt.Errorf("ispconfig: delete txt record: %w", err) + } + + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) findZone(ctx context.Context, sessionID, fqdn string) (int, error) { + for domain := range dns01.UnFqdnDomainsSeq(fqdn) { + zoneID, err := d.client.GetZoneID(ctx, sessionID, domain) + if err == nil { + return zoneID, nil + } + } + + return 0, fmt.Errorf("zone not found for %q", fqdn) +} diff --git a/providers/dns/ispconfig/ispconfig.toml b/providers/dns/ispconfig/ispconfig.toml new file mode 100644 index 000000000..4defd5509 --- /dev/null +++ b/providers/dns/ispconfig/ispconfig.toml @@ -0,0 +1,27 @@ +Name = "ISPConfig 3" +Description = '''''' +URL = "https://www.ispconfig.org/" +Code = "ispconfig" +Since = "v4.31.0" + +Example = ''' +ISPCONFIG_SERVER_URL="https://example.com:8080/remote/json.php" \ +ISPCONFIG_USERNAME="xxx" \ +ISPCONFIG_PASSWORD="yyy" \ +lego --dns ispconfig -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + ISPCONFIG_SERVER_URL = "Server URL" + ISPCONFIG_USERNAME = "Username" + ISPCONFIG_PASSWORD = "Password" + [Configuration.Additional] + ISPCONFIG_INSECURE_SKIP_VERIFY = "Whether to verify the API certificate" + ISPCONFIG_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + ISPCONFIG_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + ISPCONFIG_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + ISPCONFIG_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/index.html" diff --git a/providers/dns/ispconfig/ispconfig_test.go b/providers/dns/ispconfig/ispconfig_test.go new file mode 100644 index 000000000..b03463aee --- /dev/null +++ b/providers/dns/ispconfig/ispconfig_test.go @@ -0,0 +1,173 @@ +package ispconfig + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvServerURL, + 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{ + EnvServerURL: "https://example.com:80/", + EnvUsername: "user", + EnvPassword: "secret", + }, + }, + { + desc: "missing server URL", + envVars: map[string]string{ + EnvServerURL: "", + EnvUsername: "user", + EnvPassword: "secret", + }, + expected: "ispconfig: some credentials information are missing: ISPCONFIG_SERVER_URL", + }, + { + desc: "missing username", + envVars: map[string]string{ + EnvServerURL: "https://example.com:80/", + EnvUsername: "", + EnvPassword: "secret", + }, + expected: "ispconfig: some credentials information are missing: ISPCONFIG_USERNAME", + }, + { + desc: "missing password", + envVars: map[string]string{ + EnvServerURL: "https://example.com:80/", + EnvUsername: "user", + EnvPassword: "", + }, + expected: "ispconfig: some credentials information are missing: ISPCONFIG_PASSWORD", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "ispconfig: some credentials information are missing: ISPCONFIG_SERVER_URL,ISPCONFIG_USERNAME,ISPCONFIG_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 + serverURL string + username string + password string + expected string + }{ + { + desc: "success", + serverURL: "https://example.com:80/", + username: "user", + password: "secret", + }, + { + desc: "missing server URL", + username: "user", + password: "secret", + expected: "ispconfig: missing server URL", + }, + { + desc: "missing username", + serverURL: "https://example.com:80/", + password: "secret", + expected: "ispconfig: credentials missing", + }, + { + desc: "missing password", + serverURL: "https://example.com:80/", + username: "user", + expected: "ispconfig: credentials missing", + }, + { + desc: "missing credentials", + expected: "ispconfig: 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 + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/ispconfigddns/internal/client.go b/providers/dns/ispconfigddns/internal/client.go new file mode 100644 index 000000000..700b58f89 --- /dev/null +++ b/providers/dns/ispconfigddns/internal/client.go @@ -0,0 +1,111 @@ +package internal + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" + querystring "github.com/google/go-querystring/query" +) + +const ( + addAction = "add" + deleteAction = "delete" +) + +type Client struct { + token string + serverURL string + + HTTPClient *http.Client +} + +func NewClient(serverURL, token string) (*Client, error) { + _, err := url.Parse(serverURL) + if err != nil { + return nil, fmt.Errorf("server URL: %w", err) + } + + return &Client{ + serverURL: serverURL, + token: token, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) AddTXTRecord(ctx context.Context, zone, fqdn, content string) error { + return c.updateRecord(ctx, UpdateRecord{Action: addAction, Zone: zone, Type: "TXT", Record: fqdn, Data: content}) +} + +func (c *Client) DeleteTXTRecord(ctx context.Context, zone, fqdn, recordContent string) error { + return c.updateRecord(ctx, UpdateRecord{Action: deleteAction, Zone: zone, Type: "TXT", Record: fqdn, Data: recordContent}) +} + +func (c *Client) updateRecord(ctx context.Context, action UpdateRecord) error { + req, err := c.newRequest(ctx, action) + if err != nil { + return err + } + + return c.do(req) +} + +func (c *Client) do(req *http.Request) error { + useragent.SetHeader(req.Header) + + req.SetBasicAuth("anonymous", c.token) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + // The endpoint uses the `DefaultDdnsResponseWriter`, + // and this writer uses HTTP status code to determine if the request was successful or not. + // - https://github.com/mhofer117/ispconfig-ddns-module/blob/8b011a5bb138881d9f13360a5c4fec10c0084613/lib/updater/DdnsUpdater.php#L53-L57 + // - https://github.com/mhofer117/ispconfig-ddns-module/blob/master/lib/updater/response/DefaultDdnsResponseWriter.php + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return nil +} + +func (c *Client) newRequest(ctx context.Context, action UpdateRecord) (*http.Request, error) { + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return nil, err + } + + endpoint = endpoint.JoinPath("ddns", "update.php") + + values, err := querystring.Values(action) + if err != nil { + return nil, err + } + + endpoint.RawQuery = values.Encode() + + method := http.MethodPost + if action.Action == deleteAction { + method = http.MethodDelete + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), nil) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "application/json") + + return req, nil +} diff --git a/providers/dns/ispconfigddns/internal/client_test.go b/providers/dns/ispconfigddns/internal/client_test.go new file mode 100644 index 000000000..774e5ee46 --- /dev/null +++ b/providers/dns/ispconfigddns/internal/client_test.go @@ -0,0 +1,83 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +func setupClient(server *httptest.Server) (*Client, error) { + client, err := NewClient(server.URL, "secret") + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + + return client, nil +} + +func TestClient_AddTXTRecord(t *testing.T) { + client := servermock.NewBuilder[*Client](setupClient). + Route("POST /ddns/update.php", + servermock.Noop(), + servermock.CheckHeader(). + WithBasicAuth("anonymous", "secret"), + servermock.CheckQueryParameter().Strict(). + With("action", "add"). + With("zone", "example.com"). + With("type", "TXT"). + With("record", "_acme-challenge.example.com."). + With("data", "token"), + ). + Build(t) + + err := client.AddTXTRecord(t.Context(), "example.com", "_acme-challenge.example.com.", "token") + require.NoError(t, err) +} + +func TestClient_AddTXTRecord_error(t *testing.T) { + client := servermock.NewBuilder[*Client](setupClient). + Route("POST /ddns/update.php", + servermock.RawStringResponse("Missing or invalid token."). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + err := client.AddTXTRecord(t.Context(), "example.com", "_acme-challenge.example.com.", "token") + require.EqualError(t, err, "unexpected status code: [status code: 401] body: Missing or invalid token.") +} + +func TestClient_DeleteTXTRecord(t *testing.T) { + client := servermock.NewBuilder[*Client](setupClient). + Route("DELETE /ddns/update.php", + servermock.Noop(), + servermock.CheckHeader(). + WithBasicAuth("anonymous", "secret"), + servermock.CheckQueryParameter().Strict(). + With("action", "delete"). + With("zone", "example.com"). + With("type", "TXT"). + With("record", "_acme-challenge.example.com."). + With("data", "token"), + ). + Build(t) + + err := client.DeleteTXTRecord(t.Context(), "example.com", "_acme-challenge.example.com.", "token") + require.NoError(t, err) +} + +func TestClient_DeleteTXTRecord_error(t *testing.T) { + client := servermock.NewBuilder[*Client](setupClient). + Route("DELETE /ddns/update.php", + servermock.RawStringResponse("Missing or invalid token."). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + err := client.DeleteTXTRecord(t.Context(), "example.com", "_acme-challenge.example.com.", "token") + require.EqualError(t, err, "unexpected status code: [status code: 401] body: Missing or invalid token.") +} diff --git a/providers/dns/ispconfigddns/internal/types.go b/providers/dns/ispconfigddns/internal/types.go new file mode 100644 index 000000000..278738108 --- /dev/null +++ b/providers/dns/ispconfigddns/internal/types.go @@ -0,0 +1,9 @@ +package internal + +type UpdateRecord struct { + Action string `url:"action,omitempty"` + Zone string `url:"zone,omitempty"` + Type string `url:"type,omitempty"` + Record string `url:"record,omitempty"` + Data string `url:"data,omitempty"` +} diff --git a/providers/dns/ispconfigddns/ispconfigddns.go b/providers/dns/ispconfigddns/ispconfigddns.go new file mode 100644 index 000000000..eab5d413f --- /dev/null +++ b/providers/dns/ispconfigddns/ispconfigddns.go @@ -0,0 +1,145 @@ +// Package ispconfigddns implements a DNS provider for solving the DNS-01 challenge using ISPConfig 3 Dynamic DNS (DDNS) Module. +package ispconfigddns + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" + "github.com/go-acme/lego/v4/providers/dns/ispconfigddns/internal" +) + +// Environment variables names. +const ( + envNamespace = "ISPCONFIG_DDNS_" + + EnvServerURL = envNamespace + "SERVER_URL" + EnvToken = envNamespace + "TOKEN" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + ServerURL string + Token string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 3600), + 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 ISPConfig 3 Dynamic DNS (DDNS) Module. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvServerURL, EnvToken) + if err != nil { + return nil, fmt.Errorf("ispconfig (DDNS module): %w", err) + } + + config := NewDefaultConfig() + config.ServerURL = values[EnvServerURL] + config.Token = values[EnvToken] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for ISPConfig 3 Dynamic DNS (DDNS) Module. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("ispconfig (DDNS module): the configuration of the DNS provider is nil") + } + + if config.ServerURL == "" { + return nil, errors.New("ispconfig (DDNS module): missing server URL") + } + + if config.Token == "" { + return nil, errors.New("ispconfig (DDNS module): missing token") + } + + client, err := internal.NewClient(config.ServerURL, config.Token) + if err != nil { + return nil, fmt.Errorf("ispconfig (DDNS module): %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to control checking compliance to spec. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("ispconfig (DDNS module): could not find zone for domain %q: %w", domain, err) + } + + err = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(zone), info.EffectiveFQDN, info.Value) + if err != nil { + return fmt.Errorf("ispconfig (DDNS module): add record: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("ispconfig (DDNS module): could not find zone for domain %q: %w", domain, err) + } + + err = d.client.DeleteTXTRecord(context.Background(), dns01.UnFqdn(zone), info.EffectiveFQDN, info.Value) + if err != nil { + return fmt.Errorf("ispconfig (DDNS module): delete record: %w", err) + } + + return nil +} diff --git a/providers/dns/ispconfigddns/ispconfigddns.toml b/providers/dns/ispconfigddns/ispconfigddns.toml new file mode 100644 index 000000000..158ee9fbd --- /dev/null +++ b/providers/dns/ispconfigddns/ispconfigddns.toml @@ -0,0 +1,32 @@ +Name = "ISPConfig 3 - Dynamic DNS (DDNS) Module" +Description = '''''' +URL = "https://www.ispconfig.org/" +Code = "ispconfigddns" +Since = "v4.31.0" + +Example = ''' +ISPCONFIG_DDNS_SERVER_URL="https://panel.example.com:8080" \ +ISPCONFIG_DDNS_TOKEN=xxxxxx \ +lego --dns ispconfigddns -d '*.example.com' -d example.com run +''' + +Additional = ''' +ISPConfig DNS provider supports leveraging the [ISPConfig 3 Dynamic DNS (DDNS) Module](https://github.com/mhofer117/ispconfig-ddns-module). + +Requires the DDNS module described at https://www.ispconfig.org/ispconfig/download/ + +See https://www.howtoforge.com/community/threads/ispconfig-3-danymic-dns-ddns-module.87967/ for additional details. +''' + +[Configuration] + [Configuration.Credentials] + ISPCONFIG_DDNS_SERVER_URL = "API server URL (ex: https://panel.example.com:8080)" + ISPCONFIG_DDNS_TOKEN = "DDNS API token" + [Configuration.Additional] + ISPCONFIG_DDNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + ISPCONFIG_DDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + ISPCONFIG_DDNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" + ISPCONFIG_DDNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://github.com/mhofer117/ispconfig-ddns-module/tree/master/lib/updater" diff --git a/providers/dns/ispconfigddns/ispconfigddns_test.go b/providers/dns/ispconfigddns/ispconfigddns_test.go new file mode 100644 index 000000000..58e7a8f54 --- /dev/null +++ b/providers/dns/ispconfigddns/ispconfigddns_test.go @@ -0,0 +1,193 @@ +package ispconfigddns + +import ( + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvServerURL, EnvToken). + 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", + EnvToken: "secret", + }, + }, + { + desc: "missing server URL", + envVars: map[string]string{ + EnvServerURL: "", + EnvToken: "secret", + }, + expected: "ispconfig (DDNS module): some credentials information are missing: ISPCONFIG_DDNS_SERVER_URL", + }, + { + desc: "missing token", + envVars: map[string]string{ + EnvServerURL: "https://example.com", + EnvToken: "", + }, + expected: "ispconfig (DDNS module): some credentials information are missing: ISPCONFIG_DDNS_TOKEN", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "ispconfig (DDNS module): some credentials information are missing: ISPCONFIG_DDNS_SERVER_URL,ISPCONFIG_DDNS_TOKEN", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + serverURL string + token string + expected string + }{ + { + desc: "success", + serverURL: "https://example.com", + token: "secret", + }, + { + desc: "missing server URL", + serverURL: "", + token: "secret", + expected: "ispconfig (DDNS module): missing server URL", + }, + { + desc: "missing token", + serverURL: "https://example.com", + token: "", + expected: "ispconfig (DDNS module): missing token", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.ServerURL = test.serverURL + config.Token = test.token + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.HTTPClient = server.Client() + config.Token = "secret" + config.ServerURL = server.URL + + return NewDNSProviderConfig(config) + }, + servermock.CheckHeader(). + WithBasicAuth("anonymous", "secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /ddns/update.php", + servermock.DumpRequest(), + servermock.CheckQueryParameter().Strict(). + With("action", "add"). + With("zone", "example.com"). + With("type", "TXT"). + With("record", "_acme-challenge.example.com."). + With("data", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("DELETE /ddns/update.php", + servermock.DumpRequest(), + servermock.CheckQueryParameter().Strict(). + With("action", "delete"). + With("zone", "example.com"). + With("type", "TXT"). + With("record", "_acme-challenge.example.com."). + With("data", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/iwantmyname/iwantmyname.toml b/providers/dns/iwantmyname/iwantmyname.toml index a138dee9e..a82c2b749 100644 --- a/providers/dns/iwantmyname/iwantmyname.toml +++ b/providers/dns/iwantmyname/iwantmyname.toml @@ -11,7 +11,7 @@ Since = "v4.7.0" Example = ''' IWANTMYNAME_USERNAME=xxxxxxxx \ IWANTMYNAME_PASSWORD=xxxxxxxx \ -lego --email you@example.com --dns iwantmyname -d '*.example.com' -d example.com run +lego --dns iwantmyname -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/jdcloud/fixtures/create_record-request.json b/providers/dns/jdcloud/fixtures/create_record-request.json new file mode 100644 index 000000000..581c00fea --- /dev/null +++ b/providers/dns/jdcloud/fixtures/create_record-request.json @@ -0,0 +1,15 @@ +{ + "domainId": "20", + "regionId": "cn-north-1", + "req": { + "hostRecord": "_acme-challenge", + "hostValue": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "jcloudRes": null, + "mxPriority": null, + "port": null, + "ttl": 120, + "type": "TXT", + "viewValue": -1, + "weight": null + } +} diff --git a/providers/dns/jdcloud/fixtures/create_record.json b/providers/dns/jdcloud/fixtures/create_record.json new file mode 100644 index 000000000..08bd3db26 --- /dev/null +++ b/providers/dns/jdcloud/fixtures/create_record.json @@ -0,0 +1,25 @@ +{ + "requestId": "azerty", + "error": { + "code": 0, + "status": "", + "message": "" + }, + "result": { + "dataList": { + "id": 123, + "hostRecord": "_acme-challenge", + "hostValue": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "jcloudRes": false, + "mxPriority": 0, + "port": 0, + "ttl": 120, + "type": "TXT", + "weight": 0, + "viewValue": [ + 1, + 2 + ] + } + } +} diff --git a/providers/dns/jdcloud/fixtures/delete_record.json b/providers/dns/jdcloud/fixtures/delete_record.json new file mode 100644 index 000000000..20525751c --- /dev/null +++ b/providers/dns/jdcloud/fixtures/delete_record.json @@ -0,0 +1,9 @@ +{ + "requestId": "azerty", + "error": { + "code": 0, + "status": "", + "message": "" + }, + "result": {} +} diff --git a/providers/dns/jdcloud/fixtures/describe_domains_page1.json b/providers/dns/jdcloud/fixtures/describe_domains_page1.json new file mode 100644 index 000000000..cde6dcd6f --- /dev/null +++ b/providers/dns/jdcloud/fixtures/describe_domains_page1.json @@ -0,0 +1,55 @@ +{ + "requestId": "azerty", + "error": { + "code": 0, + "status": "", + "message": "" + }, + "result": { + "dataList": [ + { + "id": 1, + "domainName": "1.example" + }, + { + "id": 2, + "domainName": "2.example" + }, + { + "id": 3, + "domainName": "3.example" + }, + { + "id": 4, + "domainName": "4.example" + }, + { + "id": 5, + "domainName": "5.example" + }, + { + "id": 6, + "domainName": "6.example" + }, + { + "id": 7, + "domainName": "7.example" + }, + { + "id": 8, + "domainName": "8.example" + }, + { + "id": 9, + "domainName": "9.example" + }, + { + "id": 10, + "domainName": "10.example" + } + ], + "currentCount": 10, + "totalCount": 20, + "totalPage": 2 + } +} diff --git a/providers/dns/jdcloud/fixtures/describe_domains_page2.json b/providers/dns/jdcloud/fixtures/describe_domains_page2.json new file mode 100644 index 000000000..b1e1560ab --- /dev/null +++ b/providers/dns/jdcloud/fixtures/describe_domains_page2.json @@ -0,0 +1,55 @@ +{ + "requestId": "azerty", + "error": { + "code": 0, + "status": "", + "message": "" + }, + "result": { + "dataList": [ + { + "id": 11, + "domainName": "11.example" + }, + { + "id": 12, + "domainName": "12.example" + }, + { + "id": 13, + "domainName": "13.example" + }, + { + "id": 14, + "domainName": "14.example" + }, + { + "id": 15, + "domainName": "15.example" + }, + { + "id": 16, + "domainName": "16.example" + }, + { + "id": 17, + "domainName": "17.example" + }, + { + "id": 18, + "domainName": "18.example" + }, + { + "id": 19, + "domainName": "19.example" + }, + { + "id": 20, + "domainName": "example.com" + } + ], + "currentCount": 10, + "totalCount": 20, + "totalPage": 2 + } +} diff --git a/providers/dns/jdcloud/jdcloud.go b/providers/dns/jdcloud/jdcloud.go new file mode 100644 index 000000000..7d9ad4e6b --- /dev/null +++ b/providers/dns/jdcloud/jdcloud.go @@ -0,0 +1,217 @@ +// Package jdcloud implements a DNS provider for solving the DNS-01 challenge using JD Cloud. +package jdcloud + +import ( + "errors" + "fmt" + "strconv" + "sync" + "time" + + "github.com/go-acme/jdcloud-sdk-go/core" + "github.com/go-acme/jdcloud-sdk-go/services/domainservice/apis" + jdcclient "github.com/go-acme/jdcloud-sdk-go/services/domainservice/client" + domainservice "github.com/go-acme/jdcloud-sdk-go/services/domainservice/models" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" +) + +// Environment variables names. +const ( + envNamespace = "JDCLOUD_" + + EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID" + EnvAccessKeySecret = envNamespace + "ACCESS_KEY_SECRET" + EnvRegionID = envNamespace + "REGION_ID" + + 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 { + AccessKeyID string + AccessKeySecret string + RegionID string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPTimeout time.Duration +} + +// 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), + HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *jdcclient.DomainserviceClient + + recordIDs map[string]int + domainIDs map[string]int + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for JD Cloud. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAccessKeyID, EnvAccessKeySecret) + if err != nil { + return nil, fmt.Errorf("jdcloud: %w", err) + } + + config := NewDefaultConfig() + config.AccessKeyID = values[EnvAccessKeyID] + config.AccessKeySecret = values[EnvAccessKeySecret] + + // https://docs.jdcloud.com/en/common-declaration/api/introduction#Region%20Code + config.RegionID = env.GetOrDefaultString(EnvRegionID, "cn-north-1") + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for JD Cloud. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("jdcloud: the configuration of the DNS provider is nil") + } + + if config.AccessKeyID == "" || config.AccessKeySecret == "" { + return nil, errors.New("jdcloud: missing credentials") + } + + cred := core.NewCredentials(config.AccessKeyID, config.AccessKeySecret) + + client := jdcclient.NewDomainserviceClient(cred) + client.DisableLogger() + client.Config.SetTimeout(config.HTTPTimeout) + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]int), + domainIDs: make(map[string]int), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("jdcloud: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("jdcloud: %w", err) + } + + zone, err := d.findZone(dns01.UnFqdn(authZone)) + if err != nil { + return fmt.Errorf("jdcloud: %w", err) + } + + // https://docs.jdcloud.com/cn/jd-cloud-dns/api/createresourcerecord + crrr := apis.NewCreateResourceRecordRequestWithAllParams( + d.config.RegionID, + strconv.Itoa(zone.Id), + &domainservice.AddRR{ + HostRecord: subDomain, + HostValue: info.Value, + Ttl: d.config.TTL, + Type: "TXT", + ViewValue: -1, + }, + ) + + record, err := jdcclient.CreateResourceRecord(d.client, crrr) + if err != nil { + return fmt.Errorf("jdcloud: create resource record: %w", err) + } + + d.recordIDsMu.Lock() + d.domainIDs[token] = zone.Id + d.recordIDs[token] = record.Result.DataList.Id + d.recordIDsMu.Unlock() + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + d.recordIDsMu.Lock() + recordID, recordOK := d.recordIDs[token] + domainID, domainOK := d.domainIDs[token] + d.recordIDsMu.Unlock() + + if !recordOK { + return fmt.Errorf("jdcloud: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + if !domainOK { + return fmt.Errorf("jdcloud: unknown domain ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + // https://docs.jdcloud.com/cn/jd-cloud-dns/api/deleteresourcerecord + drrr := apis.NewDeleteResourceRecordRequestWithAllParams( + d.config.RegionID, + strconv.Itoa(domainID), + strconv.Itoa(recordID), + ) + + _, err := jdcclient.DeleteResourceRecord(d.client, drrr) + if err != nil { + return fmt.Errorf("jdcloud: delete resource record: %w", err) + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) findZone(zone string) (*domainservice.DomainInfo, error) { + // https://docs.jdcloud.com/cn/jd-cloud-dns/api/describedomains + ddr := apis.NewDescribeDomainsRequestWithoutParam() + ddr.SetRegionId(d.config.RegionID) + ddr.SetPageNumber(1) + ddr.SetPageSize(10) + ddr.SetDomainName(zone) + + for { + response, err := jdcclient.DescribeDomains(d.client, ddr) + if err != nil { + return nil, fmt.Errorf("describe domains: %w", err) + } + + for _, d := range response.Result.DataList { + if d.DomainName == zone { + return &d, nil + } + } + + if len(response.Result.DataList) < ddr.PageSize || response.Result.TotalPage <= ddr.PageNumber { + break + } + + ddr.SetPageNumber(ddr.PageNumber + 1) + } + + return nil, errors.New("zone not found") +} diff --git a/providers/dns/jdcloud/jdcloud.toml b/providers/dns/jdcloud/jdcloud.toml new file mode 100644 index 000000000..7ab403822 --- /dev/null +++ b/providers/dns/jdcloud/jdcloud.toml @@ -0,0 +1,27 @@ +Name = "JD Cloud" +Description = '''''' +URL = "https://www.jdcloud.com/" +Code = "jdcloud" +Since = "v4.31.0" + +Example = ''' +JDCLOUD_ACCESS_KEY_ID="xxx" \ +JDCLOUD_ACCESS_KEY_SECRET="yyy" \ +lego --dns jdcloud -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + JDCLOUD_ACCESS_KEY_ID = "Access key ID" + JDCLOUD_ACCESS_KEY_SECRET = "Access key secret" + [Configuration.Additional] + JDCLOUD_REGION_ID = "Region ID (Default: cn-north-1)" + JDCLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + JDCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + JDCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + JDCLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://docs.jdcloud.com/cn/jd-cloud-dns/api/overview" + Common = "https://docs.jdcloud.com/en/common-declaration/api/introduction" + GoClient = "https://github.com/jdcloud-api/jdcloud-sdk-go" diff --git a/providers/dns/jdcloud/jdcloud_test.go b/providers/dns/jdcloud/jdcloud_test.go new file mode 100644 index 000000000..6b3368938 --- /dev/null +++ b/providers/dns/jdcloud/jdcloud_test.go @@ -0,0 +1,242 @@ +package jdcloud + +import ( + "fmt" + "net" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvAccessKeyID, + EnvAccessKeySecret, + EnvRegionID, +).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAccessKeyID: "abc123", + EnvAccessKeySecret: "secret", + }, + }, + { + desc: "missing access key ID", + envVars: map[string]string{ + EnvAccessKeyID: "", + EnvAccessKeySecret: "secret", + }, + expected: "jdcloud: some credentials information are missing: JDCLOUD_ACCESS_KEY_ID", + }, + { + desc: "missing access key secret", + envVars: map[string]string{ + EnvAccessKeyID: "abc123", + EnvAccessKeySecret: "", + }, + expected: "jdcloud: some credentials information are missing: JDCLOUD_ACCESS_KEY_SECRET", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "jdcloud: some credentials information are missing: JDCLOUD_ACCESS_KEY_ID,JDCLOUD_ACCESS_KEY_SECRET", + }, + } + + 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 + accessKeyID string + accessKeySecret string + expected string + }{ + { + desc: "success", + accessKeyID: "abc123", + accessKeySecret: "secret", + }, + { + desc: "missing access key ID", + accessKeySecret: "secret", + expected: "jdcloud: missing credentials", + }, + { + desc: "missing access key secret", + accessKeyID: "abc123", + expected: "jdcloud: missing credentials", + }, + { + desc: "missing credentials", + expected: "jdcloud: missing credentials", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.AccessKeyID = test.accessKeyID + config.AccessKeySecret = test.accessKeySecret + config.RegionID = "cn-north-1" + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.AccessKeyID = "abc123" + config.AccessKeySecret = "secret" + config.RegionID = "cn-north-1" + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + serverURL, _ := url.Parse(server.URL) + + p.client.Config.SetEndpoint(net.JoinHostPort(serverURL.Hostname(), serverURL.Port())) + p.client.Config.SetScheme(serverURL.Scheme) + p.client.Config.SetTimeout(server.Client().Timeout) + + return p, nil + }, + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /v2/regions/cn-north-1/domain", + http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + pageNumber := req.URL.Query().Get("pageNumber") + + servermock.ResponseFromFixture( + fmt.Sprintf("describe_domains_page%s.json", pageNumber), + ).ServeHTTP(rw, req) + }), + servermock.CheckQueryParameter().Strict(). + With("domainName", "example.com"). + WithRegexp("pageNumber", `(1|2)`). + With("pageSize", "10"), + servermock.CheckHeader(). + WithRegexp("Authorization", + `JDCLOUD2-HMAC-SHA256 Credential=abc123/\d{8}/cn-north-1/domainservice/jdcloud2_request, SignedHeaders=content-type;host;x-jdcloud-date;x-jdcloud-nonce, Signature=\w+`). + WithRegexp("X-Jdcloud-Date", `\d{8}T\d{6}Z`). + WithRegexp("X-Jdcloud-Nonce", `[\w-]+`), + ). + Route("POST /v2/regions/cn-north-1/domain/20/ResourceRecord", + servermock.ResponseFromFixture("create_record.json"), + servermock.CheckRequestJSONBodyFromFixture("create_record-request.json"), + servermock.CheckHeader(). + WithRegexp("Authorization", + `JDCLOUD2-HMAC-SHA256 Credential=abc123/\d{8}/cn-north-1/domainservice/jdcloud2_request, SignedHeaders=content-type;host;x-jdcloud-date;x-jdcloud-nonce, Signature=\w+`). + WithRegexp("X-Jdcloud-Date", `\d{8}T\d{6}Z`). + WithRegexp("X-Jdcloud-Nonce", `[\w-]+`), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) + + require.Len(t, provider.domainIDs, 1) + require.Len(t, provider.recordIDs, 1) + + assert.Equal(t, 20, provider.domainIDs["abc"]) + assert.Equal(t, 123, provider.recordIDs["abc"]) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("DELETE /v2/regions/cn-north-1/domain/20/ResourceRecord/123", + servermock.ResponseFromFixture("delete_record.json"), + servermock.CheckHeader(). + WithRegexp("Authorization", + `JDCLOUD2-HMAC-SHA256 Credential=abc123/\d{8}/cn-north-1/domainservice/jdcloud2_request, SignedHeaders=content-type;host;x-jdcloud-date;x-jdcloud-nonce, Signature=\w+`). + WithRegexp("X-Jdcloud-Date", `\d{8}T\d{6}Z`). + WithRegexp("X-Jdcloud-Nonce", `[\w-]+`), + ). + Build(t) + + provider.domainIDs["abc"] = 20 + provider.recordIDs["abc"] = 123 + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/joker/joker.toml b/providers/dns/joker/joker.toml index 35713df18..20e481a6d 100644 --- a/providers/dns/joker/joker.toml +++ b/providers/dns/joker/joker.toml @@ -9,17 +9,17 @@ Example = ''' JOKER_API_MODE=SVC \ JOKER_USERNAME= \ JOKER_PASSWORD= \ -lego --email you@example.com --dns joker -d '*.example.com' -d example.com run +lego --dns joker -d '*.example.com' -d example.com run # DMAPI JOKER_API_MODE=DMAPI \ JOKER_USERNAME= \ JOKER_PASSWORD= \ -lego --email you@example.com --dns joker -d '*.example.com' -d example.com run +lego --dns joker -d '*.example.com' -d example.com run ## or JOKER_API_MODE=DMAPI \ JOKER_API_KEY= \ -lego --email you@example.com --dns joker -d '*.example.com' -d example.com run +lego --dns joker -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/keyhelp/keyhelp.toml b/providers/dns/keyhelp/keyhelp.toml index d6f84e34e..e622794ca 100644 --- a/providers/dns/keyhelp/keyhelp.toml +++ b/providers/dns/keyhelp/keyhelp.toml @@ -7,7 +7,7 @@ Since = "v4.26.0" Example = ''' KEYHELP_BASE_URL="https://keyhelp.example.com" \ KEYHELP_API_KEY="xxx" \ -lego --email you@example.com --dns keyhelp -d '*.example.com' -d example.com run +lego --dns keyhelp -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/leaseweb/internal/client.go b/providers/dns/leaseweb/internal/client.go new file mode 100644 index 000000000..01619d49b --- /dev/null +++ b/providers/dns/leaseweb/internal/client.go @@ -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/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/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 +} diff --git a/providers/dns/leaseweb/internal/client_test.go b/providers/dns/leaseweb/internal/client_test.go new file mode 100644 index 000000000..5762aad4b --- /dev/null +++ b/providers/dns/leaseweb/internal/client_test.go @@ -0,0 +1,149 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("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)") +} diff --git a/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet-request.json b/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet-request.json new file mode 100644 index 000000000..af53fcf04 --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet-request.json @@ -0,0 +1,8 @@ +{ + "content": [ + "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" + ], + "name": "_acme-challenge.example.com.", + "ttl": 300, + "type": "TXT" +} diff --git a/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet.json b/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet.json new file mode 100644 index 000000000..8ca040d63 --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet.json @@ -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" +} diff --git a/providers/dns/leaseweb/internal/fixtures/error_400.json b/providers/dns/leaseweb/internal/fixtures/error_400.json new file mode 100644 index 000000000..1a980b6bb --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/error_400.json @@ -0,0 +1,6 @@ +{ + "correlationId": "289346a1-3eaf-4da4-b707-62ef12eb08be", + "errorCode": "400", + "errorDetails": {}, + "errorMessage": "The API could not interpret your request correctly." +} diff --git a/providers/dns/leaseweb/internal/fixtures/error_401.json b/providers/dns/leaseweb/internal/fixtures/error_401.json new file mode 100644 index 000000000..47d8a311d --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/error_401.json @@ -0,0 +1,5 @@ +{ + "correlationId": "289346a1-3eaf-4da4-b707-62ef12eb08be", + "errorCode": "401", + "errorMessage": "You are not authorized to view this resource." +} diff --git a/providers/dns/leaseweb/internal/fixtures/error_404.json b/providers/dns/leaseweb/internal/fixtures/error_404.json new file mode 100644 index 000000000..1deaf5606 --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/error_404.json @@ -0,0 +1,5 @@ +{ + "correlationId": "289346a1-3eaf-4da4-b707-62ef12eb08be", + "errorCode": "404", + "errorMessage": "Resource not found" +} diff --git a/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet.json b/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet.json new file mode 100644 index 000000000..fd48f60c6 --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet.json @@ -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" +} diff --git a/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet2.json b/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet2.json new file mode 100644 index 000000000..abf3fb4c3 --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet2.json @@ -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" +} diff --git a/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request.json b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request.json new file mode 100644 index 000000000..e781958c8 --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request.json @@ -0,0 +1,8 @@ +{ + "content": [ + "foo", + "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo", + "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" + ], + "ttl": 3600 +} diff --git a/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request2.json b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request2.json new file mode 100644 index 000000000..0acc314de --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request2.json @@ -0,0 +1,6 @@ +{ + "content": [ + "foo" + ], + "ttl": 3600 +} diff --git a/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet.json b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet.json new file mode 100644 index 000000000..2b877982c --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet.json @@ -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" +} diff --git a/providers/dns/leaseweb/internal/types.go b/providers/dns/leaseweb/internal/types.go new file mode 100644 index 000000000..7a4547584 --- /dev/null +++ b/providers/dns/leaseweb/internal/types.go @@ -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"` +} diff --git a/providers/dns/leaseweb/leaseweb.go b/providers/dns/leaseweb/leaseweb.go new file mode 100644 index 000000000..fafaf1c4d --- /dev/null +++ b/providers/dns/leaseweb/leaseweb.go @@ -0,0 +1,187 @@ +// 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/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" + "github.com/go-acme/lego/v4/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(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("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(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("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 +} diff --git a/providers/dns/leaseweb/leaseweb.toml b/providers/dns/leaseweb/leaseweb.toml new file mode 100644 index 000000000..2c3503291 --- /dev/null +++ b/providers/dns/leaseweb/leaseweb.toml @@ -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" diff --git a/providers/dns/leaseweb/leaseweb_test.go b/providers/dns/leaseweb/leaseweb_test.go new file mode 100644 index 000000000..0450cd2c2 --- /dev/null +++ b/providers/dns/leaseweb/leaseweb_test.go @@ -0,0 +1,204 @@ +package leaseweb + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/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(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.APIKey = "secret" + config.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("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("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("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("example.com", "abc", "1234d==") + require.NoError(t, err) +} diff --git a/providers/dns/liara/internal/client.go b/providers/dns/liara/internal/client.go index 93cdcf7c8..95c39695b 100644 --- a/providers/dns/liara/internal/client.go +++ b/providers/dns/liara/internal/client.go @@ -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 { diff --git a/providers/dns/liara/internal/client_test.go b/providers/dns/liara/internal/client_test.go index 57ac7e8b3..b6d007046 100644 --- a/providers/dns/liara/internal/client_test.go +++ b/providers/dns/liara/internal/client_test.go @@ -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)). diff --git a/providers/dns/liara/liara.go b/providers/dns/liara/liara.go index b91b004cc..c7e403eed 100644 --- a/providers/dns/liara/liara.go +++ b/providers/dns/liara/liara.go @@ -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{ diff --git a/providers/dns/liara/liara.toml b/providers/dns/liara/liara.toml index 1259999a2..f471de04e 100644 --- a/providers/dns/liara/liara.toml +++ b/providers/dns/liara/liara.toml @@ -6,13 +6,14 @@ Since = "v4.10.0" Example = ''' LIARA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns liara -d '*.example.com' -d example.com run +lego --dns liara -d '*.example.com' -d example.com run ''' [Configuration] [Configuration.Credentials] LIARA_API_KEY = "The API key" [Configuration.Additional] + LIARA_TEAM_ID = "The team ID to access services in a team" LIARA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" LIARA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" LIARA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" diff --git a/providers/dns/limacity/limacity.toml b/providers/dns/limacity/limacity.toml index b9b9f0018..d236577d0 100644 --- a/providers/dns/limacity/limacity.toml +++ b/providers/dns/limacity/limacity.toml @@ -6,7 +6,7 @@ Since = "v4.18.0" Example = ''' LIMACITY_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns limacity -d '*.example.com' -d example.com run +lego --dns limacity -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/linode/linode.toml b/providers/dns/linode/linode.toml index f046d3f9b..9ea30b92b 100644 --- a/providers/dns/linode/linode.toml +++ b/providers/dns/linode/linode.toml @@ -7,7 +7,7 @@ Since = "v1.1.0" Example = ''' LINODE_TOKEN=xxxxx \ -lego --email you@example.com --dns linode -d '*.example.com' -d example.com run +lego --dns linode -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/liquidweb/liquidweb.toml b/providers/dns/liquidweb/liquidweb.toml index 22789f41e..386b99cab 100644 --- a/providers/dns/liquidweb/liquidweb.toml +++ b/providers/dns/liquidweb/liquidweb.toml @@ -7,7 +7,7 @@ Since = "v3.1.0" Example = ''' LWAPI_USERNAME=someuser \ LWAPI_PASSWORD=somepass \ -lego --email you@example.com --dns liquidweb -d '*.example.com' -d example.com run +lego --dns liquidweb -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/liquidweb/liquidweb_test.go b/providers/dns/liquidweb/liquidweb_test.go index 26dc5bdc0..a34d19037 100644 --- a/providers/dns/liquidweb/liquidweb_test.go +++ b/providers/dns/liquidweb/liquidweb_test.go @@ -27,16 +27,16 @@ func TestNewDNSProvider(t *testing.T) { { desc: "minimum-success", envVars: map[string]string{ - EnvUsername: "blars", - EnvPassword: "tacoman", + EnvUsername: "user", + EnvPassword: "secret", }, }, { desc: "set-everything", envVars: map[string]string{ - EnvURL: "https://storm.com", - EnvUsername: "blars", - EnvPassword: "tacoman", + EnvURL: "https://storm.example", + EnvUsername: "user", + EnvPassword: "secret", EnvZone: "blars.com", }, }, @@ -48,16 +48,16 @@ func TestNewDNSProvider(t *testing.T) { { desc: "missing username", envVars: map[string]string{ - EnvPassword: "tacoman", - EnvZone: "blars.com", + EnvPassword: "secret", + EnvZone: "blars.example", }, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_USERNAME", }, { desc: "missing password", envVars: map[string]string{ - EnvUsername: "blars", - EnvZone: "blars.com", + EnvUsername: "user", + EnvZone: "blars.example", }, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_PASSWORD", }, @@ -148,13 +148,13 @@ func TestNewDNSProviderConfig(t *testing.T) { func TestDNSProvider_Present(t *testing.T) { provider := mockProvider(t) - err := provider.Present("tacoman.com", "", "") + err := provider.Present("tacoman.example", "", "") require.NoError(t, err) } func TestDNSProvider_CleanUp(t *testing.T) { provider := mockProvider(t, network.DNSRecord{ - Name: "_acme-challenge.tacoman.com", + Name: "_acme-challenge.tacoman.example", RData: "123d==", Type: "TXT", TTL: 300, @@ -164,7 +164,7 @@ func TestDNSProvider_CleanUp(t *testing.T) { provider.recordIDs["123d=="] = 1234567 - err := provider.CleanUp("tacoman.com.", "123d==", "") + err := provider.CleanUp("tacoman.example.", "123d==", "") require.NoError(t, err) } @@ -181,7 +181,7 @@ func TestDNSProvider(t *testing.T) { }{ { desc: "expected successful", - domain: "tacoman.com", + domain: "tacoman.example", token: "123", keyAuth: "456", present: true, @@ -189,7 +189,7 @@ func TestDNSProvider(t *testing.T) { }, { desc: "other successful", - domain: "banana.com", + domain: "banana.example", token: "123", keyAuth: "456", present: true, @@ -197,16 +197,16 @@ func TestDNSProvider(t *testing.T) { }, { desc: "zone not on account", - domain: "huckleberry.com", + domain: "huckleberry.example", token: "123", keyAuth: "456", present: true, - expPresentErr: "no valid zone in account for certificate '_acme-challenge.huckleberry.com'", + expPresentErr: "no valid zone in account for certificate '_acme-challenge.huckleberry.example'", cleanup: false, }, { desc: "ssl for domain", - domain: "sundae.cherry.com", + domain: "sundae.cherry.example", token: "5847953", keyAuth: "34872934", present: true, @@ -214,7 +214,7 @@ func TestDNSProvider(t *testing.T) { }, { desc: "complicated domain", - domain: "always.money.stand.banana.com", + domain: "always.money.stand.banana.example", token: "5847953", keyAuth: "there is always money in the banana stand", present: true, diff --git a/providers/dns/liquidweb/servermock_test.go b/providers/dns/liquidweb/servermock_test.go index f211e7253..4886e17f1 100644 --- a/providers/dns/liquidweb/servermock_test.go +++ b/providers/dns/liquidweb/servermock_test.go @@ -26,14 +26,14 @@ func mockProvider(t *testing.T, initRecs ...network.DNSRecord) *DNSProvider { return servermock.NewBuilder( func(server *httptest.Server) (*DNSProvider, error) { config := NewDefaultConfig() - config.Username = "blars" - config.Password = "tacoman" + config.Username = "user" + config.Password = "secret" config.BaseURL = server.URL return NewDNSProviderConfig(config) }, servermock.CheckHeader(). - WithBasicAuth("blars", "tacoman"), + WithBasicAuth("user", "secret"), ). Route("/v1/Network/DNS/Record/delete", mockAPIDelete(recs)). Route("/v1/Network/DNS/Record/create", mockAPICreate(recs)). @@ -172,38 +172,38 @@ func makeMockZones() (map[int]network.DNSZoneList, map[string]int) { Items: []network.DNSZone{ { ID: 1, - Name: "blars.com", + Name: "blars.example", Active: 1, DelegationStatus: "CORRECT", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 2, - Name: "tacoman.com", + Name: "tacoman.example", Active: 1, DelegationStatus: "CORRECT", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 3, - Name: "storm.com", + Name: "storm.example", Active: 1, DelegationStatus: "CORRECT", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 4, - Name: "not-apple.com", + Name: "not-apple.example", Active: 1, DelegationStatus: "BAD_NAMESERVERS", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 5, Name: "example.com", Active: 1, DelegationStatus: "BAD_NAMESERVERS", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, }, }, @@ -211,38 +211,38 @@ func makeMockZones() (map[int]network.DNSZoneList, map[string]int) { Items: []network.DNSZone{ { ID: 6, - Name: "banana.com", + Name: "banana.example", Active: 1, DelegationStatus: "NXDOMAIN", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 7, - Name: "cherry.com", + Name: "cherry.example", Active: 1, DelegationStatus: "SERVFAIL", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 8, - Name: "dates.com", + Name: "dates.example", Active: 1, DelegationStatus: "SERVFAIL", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 9, - Name: "eggplant.com", + Name: "eggplant.example", Active: 1, DelegationStatus: "SERVFAIL", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 10, - Name: "fig.com", + Name: "fig.example", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, }, }, @@ -250,31 +250,31 @@ func makeMockZones() (map[int]network.DNSZoneList, map[string]int) { Items: []network.DNSZone{ { ID: 11, - Name: "grapes.com", + Name: "grapes.example", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 12, - Name: "money.banana.com", + Name: "money.banana.example", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 13, - Name: "money.stand.banana.com", + Name: "money.stand.banana.example", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 14, - Name: "stand.banana.com", + Name: "stand.banana.example", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, }, }, diff --git a/providers/dns/loopia/loopia.toml b/providers/dns/loopia/loopia.toml index 4a127ec55..a201852c9 100644 --- a/providers/dns/loopia/loopia.toml +++ b/providers/dns/loopia/loopia.toml @@ -7,7 +7,7 @@ Since = "v4.2.0" Example = ''' LOOPIA_API_USER=xxxxxxxx \ LOOPIA_API_PASSWORD=yyyyyyyy \ -lego --email you@example.com --dns loopia -d '*.example.com' -d example.com run +lego --dns loopia -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/luadns/luadns.toml b/providers/dns/luadns/luadns.toml index c80929c21..e56fac0b6 100644 --- a/providers/dns/luadns/luadns.toml +++ b/providers/dns/luadns/luadns.toml @@ -7,7 +7,7 @@ Since = "v3.7.0" Example = ''' LUADNS_API_USERNAME=youremail \ LUADNS_API_TOKEN=xxxxxxxx \ -lego --email you@example.com --dns luadns -d '*.example.com' -d example.com run +lego --dns luadns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/mailinabox/mailinabox.toml b/providers/dns/mailinabox/mailinabox.toml index e0072ebdd..74d8aabbc 100644 --- a/providers/dns/mailinabox/mailinabox.toml +++ b/providers/dns/mailinabox/mailinabox.toml @@ -8,7 +8,7 @@ Example = ''' MAILINABOX_EMAIL=user@example.com \ MAILINABOX_PASSWORD=yyyy \ MAILINABOX_BASE_URL=https://box.example.com \ -lego --email you@example.com --dns mailinabox -d '*.example.com' -d example.com run +lego --dns mailinabox -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/manageengine/manageengine.toml b/providers/dns/manageengine/manageengine.toml index 7708fa74f..43a782841 100644 --- a/providers/dns/manageengine/manageengine.toml +++ b/providers/dns/manageengine/manageengine.toml @@ -7,7 +7,7 @@ Since = "v4.21.0" Example = ''' MANAGEENGINE_CLIENT_ID="xxx" \ MANAGEENGINE_CLIENT_SECRET="yyy" \ -lego --email you@example.com --dns manageengine -d '*.example.com' -d example.com run +lego --dns manageengine -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/manual/manual.toml b/providers/dns/manual/manual.toml index 88acf4750..fc47a8fae 100644 --- a/providers/dns/manual/manual.toml +++ b/providers/dns/manual/manual.toml @@ -4,7 +4,7 @@ Code = "manual" Since = "v0.3.0" Example = ''' -lego --email you@example.com --dns manual -d '*.example.com' -d example.com run +lego --dns manual -d '*.example.com' -d example.com run ''' Additional = ''' @@ -13,7 +13,7 @@ Additional = ''' To start using the CLI prompt "provider", start lego with `--dns manual`: ```console -$ lego --email "you@example.com" --domains="example.com" --dns "manual" run +$ lego --dns manual -d example.com run ``` What follows are a few log print-outs, interspersed with some prompts, asking for you to do perform some actions: @@ -31,13 +31,13 @@ If you accept the linked Terms of Service, hit `Enter`. [INFO] acme: Registering account for you@example.com !!!! HEADS UP !!!! - Your account credentials have been saved in your Let's Encrypt - configuration directory at "./.lego/accounts". +Your account credentials have been saved in your +configuration directory at "./.lego/accounts". - You should make a secure backup of this folder now. This - configuration directory will also contain certificates and - private keys obtained from Let's Encrypt so making regular - backups of this folder is ideal. +You should make a secure backup of this folder now. This +configuration directory will also contain private keys +generated by lego and certificates obtained from the ACME +server. Making regular backups of this folder is ideal. [INFO] [example.com] acme: Obtaining bundled SAN certificate [INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/2345678901 [INFO] [example.com] acme: Could not find solver for: tls-alpn-01 diff --git a/providers/dns/metaname/metaname.toml b/providers/dns/metaname/metaname.toml index 4a147d043..654dcaed0 100644 --- a/providers/dns/metaname/metaname.toml +++ b/providers/dns/metaname/metaname.toml @@ -7,7 +7,7 @@ Since = "v4.13.0" Example = ''' METANAME_ACCOUNT_REFERENCE=xxxx \ METANAME_API_KEY=yyyyyyy \ -lego --email you@example.com --dns metaname -d '*.example.com' -d example.com run +lego --dns metaname -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/metaregistrar/metaregistrar.toml b/providers/dns/metaregistrar/metaregistrar.toml index 952c7ea61..e505e0ce2 100644 --- a/providers/dns/metaregistrar/metaregistrar.toml +++ b/providers/dns/metaregistrar/metaregistrar.toml @@ -6,7 +6,7 @@ Since = "v4.23.0" Example = ''' METAREGISTRAR_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns metaregistrar -d '*.example.com' -d example.com run +lego --dns metaregistrar -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/mijnhost/mijnhost.toml b/providers/dns/mijnhost/mijnhost.toml index 00152e132..416fdde53 100644 --- a/providers/dns/mijnhost/mijnhost.toml +++ b/providers/dns/mijnhost/mijnhost.toml @@ -6,7 +6,7 @@ Since = "v4.18.0" Example = ''' MIJNHOST_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns mijnhost -d '*.example.com' -d example.com run +lego --dns mijnhost -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/mittwald/internal/types.go b/providers/dns/mittwald/internal/types.go index ce49cb820..86cdf065c 100644 --- a/providers/dns/mittwald/internal/types.go +++ b/providers/dns/mittwald/internal/types.go @@ -61,14 +61,14 @@ type APIError struct { } func (a APIError) Error() string { - var msg strings.Builder + msg := new(strings.Builder) - msg.WriteString(fmt.Sprintf("%s: %s", a.Type, a.Message)) + _, _ = fmt.Fprintf(msg, "%s: %s", a.Type, a.Message) if len(a.ValidationErrors) > 0 { for _, validationError := range a.ValidationErrors { - msg.WriteString(fmt.Sprintf(" [%s: %s (%s, %s)]", - validationError.Type, validationError.Message, validationError.Path, validationError.Context.Format)) + _, _ = fmt.Fprintf(msg, " [%s: %s (%s, %s)]", + validationError.Type, validationError.Message, validationError.Path, validationError.Context.Format) } } diff --git a/providers/dns/mittwald/mittwald.toml b/providers/dns/mittwald/mittwald.toml index 937b9c172..36a9f6c16 100644 --- a/providers/dns/mittwald/mittwald.toml +++ b/providers/dns/mittwald/mittwald.toml @@ -6,7 +6,7 @@ Since = "v1.48.0" Example = ''' MITTWALD_TOKEN=my-token \ -lego --email you@example.com --dns mittwald -d '*.example.com' -d example.com run +lego --dns mittwald -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/myaddr/myaddr.toml b/providers/dns/myaddr/myaddr.toml index 5ff306526..2f5fe6c1f 100644 --- a/providers/dns/myaddr/myaddr.toml +++ b/providers/dns/myaddr/myaddr.toml @@ -6,7 +6,7 @@ Since = "v4.22.0" Example = ''' MYADDR_PRIVATE_KEYS_MAPPING="example:123,test:456" \ -lego --email you@example.com --dns myaddr -d '*.example.com' -d example.com run +lego --dns myaddr -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/mydnsjp/mydnsjp.toml b/providers/dns/mydnsjp/mydnsjp.toml index ab842e37f..eb9e73acc 100644 --- a/providers/dns/mydnsjp/mydnsjp.toml +++ b/providers/dns/mydnsjp/mydnsjp.toml @@ -7,7 +7,7 @@ Since = "v1.2.0" Example = ''' MYDNSJP_MASTER_ID=xxxxx \ MYDNSJP_PASSWORD=xxxxx \ -lego --email you@example.com --dns mydnsjp -d '*.example.com' -d example.com run +lego --dns mydnsjp -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/mythicbeasts/mythicbeasts.toml b/providers/dns/mythicbeasts/mythicbeasts.toml index 011abba1f..cada3041d 100644 --- a/providers/dns/mythicbeasts/mythicbeasts.toml +++ b/providers/dns/mythicbeasts/mythicbeasts.toml @@ -7,7 +7,7 @@ Since = "v0.3.7" Example = ''' MYTHICBEASTS_USERNAME=myuser \ MYTHICBEASTS_PASSWORD=mypass \ -lego --email you@example.com --dns mythicbeasts -d '*.example.com' -d example.com run +lego --dns mythicbeasts -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/namecheap/namecheap.toml b/providers/dns/namecheap/namecheap.toml index 3a5be870c..b0f92a1bd 100644 --- a/providers/dns/namecheap/namecheap.toml +++ b/providers/dns/namecheap/namecheap.toml @@ -14,7 +14,7 @@ More information in the section [Enabling API Access](https://www.namecheap.com/ Example = ''' NAMECHEAP_API_USER=user \ NAMECHEAP_API_KEY=key \ -lego --email you@example.com --dns namecheap -d '*.example.com' -d example.com run +lego --dns namecheap -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/namedotcom/namedotcom.go b/providers/dns/namedotcom/namedotcom.go index 3d1f33af1..04c8b5967 100644 --- a/providers/dns/namedotcom/namedotcom.go +++ b/providers/dns/namedotcom/namedotcom.go @@ -116,7 +116,10 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - // TODO(ldez) replace domain by FQDN to follow CNAME. + if info.EffectiveFQDN != info.FQDN { + domain = dns01.UnFqdn(info.EffectiveFQDN) + } + domainDetails, err := d.client.GetDomain(&namecom.GetDomainRequest{DomainName: domain}) if err != nil { return fmt.Errorf("namedotcom: API call failed: %w", err) @@ -127,7 +130,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("namedotcom: %w", err) } - // TODO(ldez) replace domain by FQDN to follow CNAME. request := &namecom.Record{ DomainName: domain, Host: subDomain, @@ -148,7 +150,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - // TODO(ldez) replace domain by FQDN to follow CNAME. + if info.EffectiveFQDN != info.FQDN { + domain = dns01.UnFqdn(info.EffectiveFQDN) + } + records, err := d.getRecords(domain) if err != nil { return fmt.Errorf("namedotcom: %w", err) @@ -156,7 +161,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { for _, rec := range records { if rec.Fqdn == info.EffectiveFQDN && rec.Type == "TXT" { - // TODO(ldez) replace domain by FQDN to follow CNAME. request := &namecom.DeleteRecordRequest{ DomainName: domain, ID: rec.ID, diff --git a/providers/dns/namedotcom/namedotcom.toml b/providers/dns/namedotcom/namedotcom.toml index e6de796d1..3651c424b 100644 --- a/providers/dns/namedotcom/namedotcom.toml +++ b/providers/dns/namedotcom/namedotcom.toml @@ -7,7 +7,7 @@ Since = "v0.5.0" Example = ''' NAMECOM_USERNAME=foo.bar \ NAMECOM_API_TOKEN=a379a6f6eeafb9a55e378c118034e2751e682fab \ -lego --email you@example.com --dns namedotcom -d '*.example.com' -d example.com run +lego --dns namedotcom -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/namesilo/namesilo.toml b/providers/dns/namesilo/namesilo.toml index bab7905bf..113ddb5c5 100644 --- a/providers/dns/namesilo/namesilo.toml +++ b/providers/dns/namesilo/namesilo.toml @@ -6,7 +6,7 @@ Since = "v2.7.0" Example = ''' NAMESILO_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ -lego --email you@example.com --dns namesilo -d '*.example.com' -d example.com run +lego --dns namesilo -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/namesurfer/internal/client.go b/providers/dns/namesurfer/internal/client.go new file mode 100644 index 000000000..e40a7988c --- /dev/null +++ b/providers/dns/namesurfer/internal/client.go @@ -0,0 +1,226 @@ +package internal + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "slices" + "strconv" + "strings" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" +) + +type Client struct { + apiKey string + apiSecret string + + BaseURL *url.URL + HTTPClient *http.Client +} + +func NewClient(baseURL, apiKey, apiSecret string) (*Client, error) { + if apiKey == "" || apiSecret == "" { + return nil, errors.New("credentials missing") + } + + if baseURL == "" { + return nil, errors.New("base URL missing") + } + + apiEndpoint, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + + return &Client{ + apiKey: apiKey, + apiSecret: apiSecret, + BaseURL: apiEndpoint.JoinPath("jsonrpc10"), + HTTPClient: &http.Client{ + Timeout: 5 * time.Second, + }, + }, nil +} + +// AddDNSRecord adds a DNS record. +// http://95.128.3.201:8053/API/NSService_10#addDNSRecord +func (d *Client) AddDNSRecord(ctx context.Context, zoneName, viewName string, record DNSNode) error { + digest := d.computeDigest( + zoneName, + viewName, + record.Name, + record.Type, + strconv.Itoa(record.TTL), + record.Data, + ) + + // JSON-RPC 1.0 requires positional parameters array + params := []any{ + digest, + zoneName, + viewName, + record, + } + + var ok bool + + err := d.doRequest(ctx, "addDNSRecord", params, &ok) + if err != nil { + return err + } + + if !ok { + return errors.New("addDNSRecord failed") + } + + return nil +} + +// UpdateDNSHost updates a DNS host record. +// Passing an empty newNode removes the oldNode. +// http://95.128.3.201:8053/API/NSService_10#updateDNSHost +func (d *Client) UpdateDNSHost(ctx context.Context, zoneName, viewName string, oldNode, newNode DNSNode) error { + digest := d.computeDigest(zoneName, viewName) + + // JSON-RPC 1.0 requires positional parameters array + params := []any{ + digest, + zoneName, + viewName, + oldNode, + newNode, + } + + var ok bool + + err := d.doRequest(ctx, "updateDNSHost", params, &ok) + if err != nil { + return err + } + + if !ok { + return errors.New("updateDNSHost failed") + } + + return nil +} + +// SearchDNSHosts searches for DNS host records. +// http://95.128.3.201:8053/API/NSService_10#searchDNSHosts +func (d *Client) SearchDNSHosts(ctx context.Context, pattern string) ([]DNSNode, error) { + digest := d.computeDigest(pattern) + + // JSON-RPC 1.0 requires positional parameters array + params := []any{ + digest, + pattern, + } + + var nodes []DNSNode + + err := d.doRequest(ctx, "searchDNSHosts", params, &nodes) + if err != nil { + return nil, err + } + + return nodes, nil +} + +// ListZones lists DNS zones. +// http://95.128.3.201:8053/API/NSService_10#listZones +func (d *Client) ListZones(ctx context.Context, mode string) ([]DNSZone, error) { + digest := d.computeDigest() + + // JSON-RPC 1.0 requires positional parameters array + params := []any{ + digest, + mode, + } + + var zones []DNSZone + + err := d.doRequest(ctx, "listZones", params, &zones) + if err != nil { + return nil, err + } + + return zones, nil +} + +func (d *Client) doRequest(ctx context.Context, method string, params []any, result any) error { + payload := APIRequest{ + ID: 1, + Method: method, + Params: slices.Concat([]any{d.apiKey}, params), + } + + buf := new(bytes.Buffer) + + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return fmt.Errorf("failed to create request JSON body: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, d.BaseURL.String(), buf) + if err != nil { + return fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + resp, err := d.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + if resp.StatusCode/100 != 2 { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + var rpcResp APIResponse + + err = json.Unmarshal(raw, &rpcResp) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + if rpcResp.Error != nil { + return rpcResp.Error + } + + err = json.Unmarshal(rpcResp.Result, result) + if err != nil { + return fmt.Errorf("unable to unmarshal response: %w: %s", err, rpcResp.Result) + } + + return nil +} + +func (d *Client) computeDigest(parts ...string) string { + params := []string{d.apiKey} + params = append(params, parts...) + params = append(params, d.apiSecret) + + mac := hmac.New(sha256.New, []byte(d.apiSecret)) + mac.Write([]byte(strings.Join(params, "&"))) + + return hex.EncodeToString(mac.Sum(nil)) +} diff --git a/providers/dns/namesurfer/internal/client_test.go b/providers/dns/namesurfer/internal/client_test.go new file mode 100644 index 000000000..9e8f917bc --- /dev/null +++ b/providers/dns/namesurfer/internal/client_test.go @@ -0,0 +1,158 @@ +package internal + +import ( + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(server.URL, "user", "secret") + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + ) +} + +func TestClient_AddDNSRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /jsonrpc10", + servermock.ResponseFromFixture("addDNSRecord.json"), + servermock.CheckRequestJSONBodyFromFixture("addDNSRecord-request.json"), + ). + Build(t) + + record := DNSNode{ + Name: "_acme-challenge", + Type: "TXT", + Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 300, + } + + err := client.AddDNSRecord(t.Context(), "example.com", "viewA", record) + require.NoError(t, err) +} + +func TestClient_AddDNSRecord_error(t *testing.T) { + client := mockBuilder(). + Route("POST /jsonrpc10", + servermock.ResponseFromFixture("error.json"), + ). + Build(t) + + record := DNSNode{ + Name: "_acme-challenge", + Type: "TXT", + Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 300, + } + + err := client.AddDNSRecord(t.Context(), "example.com", "viewA", record) + require.EqualError(t, err, "code: Server.Keyfailure, "+ + "filename: service, line: 13, "+ + "message: Unknown keyname user, "+ + `detail: Traceback (most recent call last): File "/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py", line 159, in dispatch_request result = self.call_method(method,req_dict,tc,export_dict,log_line) File "/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py", line 96, in call_method result = getattr(service_class_instance,req_dict['methodname'])(*args) File "/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/ladonizer/decorator.py", line 77, in injector res = f(*args,**kw) File "/usr/local/namesurfer/webui2/webui/service/service10/NSService_10.py", line 502, in addDNSRecord key = validate_key(keyname, digest, [zonename, viewname, record.name, record.type, str(record.ttl), record.data]) File "/usr/local/namesurfer/webui2/webui/service/base/implementation.py", line 63, in validate_key raise ApiFault('Server.Keyfailure', 'Unknown keyname %s' % keyname) ApiFault: service(13): Unknown keyname user `) +} + +func TestClient_UpdateDNSHost(t *testing.T) { + client := mockBuilder(). + Route("POST /jsonrpc10", + servermock.ResponseFromFixture("updateDNSHost.json"), + servermock.CheckRequestJSONBodyFromFixture("updateDNSHost-request.json"), + ). + Build(t) + + record := DNSNode{ + Name: "_acme-challenge", + Type: "TXT", + Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 300, + } + + err := client.UpdateDNSHost(t.Context(), "example.com", "viewA", record, DNSNode{}) + require.NoError(t, err) +} + +func TestClient_SearchDNSHosts(t *testing.T) { + client := mockBuilder(). + Route("POST /jsonrpc10", + servermock.ResponseFromFixture("searchDNSHosts.json"), + servermock.CheckRequestJSONBodyFromFixture("searchDNSHosts-request.json"), + ). + Build(t) + + records, err := client.SearchDNSHosts(t.Context(), "value") + require.NoError(t, err) + + expected := []DNSNode{ + {Name: "foo", Type: "TXT", Data: "xxx", TTL: 300}, + {Name: "_acme-challenge", Type: "TXT", Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", TTL: 300}, + {Name: "bar", Type: "A", Data: "yyy", TTL: 300}, + } + + assert.Equal(t, expected, records) +} + +func TestClient_ListZones(t *testing.T) { + client := mockBuilder(). + Route("POST /jsonrpc10", + servermock.ResponseFromFixture("listZones.json"), + servermock.CheckRequestJSONBodyFromFixture("listZones-request.json"), + ). + Build(t) + + zones, err := client.ListZones(t.Context(), "value") + require.NoError(t, err) + + expected := []DNSZone{ + {Name: "example.com", View: "viewA"}, + {Name: "example.org", View: "viewB"}, + {Name: "example.net", View: "viewC"}, + } + + assert.Equal(t, expected, zones) +} + +func TestClient_computeDigest(t *testing.T) { + client, err := NewClient("https://test.example.com", "testkey", "testsecret") + require.NoError(t, err) + + testCases := []struct { + desc string + parts []string + expected string + }{ + { + desc: "no parts", + parts: []string{}, + expected: "99b5dcdc19bfc0ce2af3fe848f4bcb6f7beb352e9599e8ba50544d86de567282", + }, + { + desc: "parts", + parts: []string{"zone.example.com", "default"}, + expected: "94efef76383889b1ae620582a25d1c3aa9bd9ba9ac4bdccdf4aefbc3ae6e8329", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + digest := client.computeDigest(test.parts...) + + assert.Equal(t, test.expected, digest) + }) + } +} diff --git a/providers/dns/namesurfer/internal/fixtures/addDNSRecord-request.json b/providers/dns/namesurfer/internal/fixtures/addDNSRecord-request.json new file mode 100644 index 000000000..660109aae --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/addDNSRecord-request.json @@ -0,0 +1,16 @@ +{ + "id": 1, + "method": "addDNSRecord", + "params": [ + "user", + "4fcc5fa29531709b0381c8debea127a6a26e71cb9491727876819cf5805c4990", + "example.com", + "viewA", + { + "name": "_acme-challenge", + "type": "TXT", + "data": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 300 + } + ] +} diff --git a/providers/dns/namesurfer/internal/fixtures/addDNSRecord.json b/providers/dns/namesurfer/internal/fixtures/addDNSRecord.json new file mode 100644 index 000000000..f41779e30 --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/addDNSRecord.json @@ -0,0 +1,4 @@ +{ + "id": 1, + "result": true +} diff --git a/providers/dns/namesurfer/internal/fixtures/error.json b/providers/dns/namesurfer/internal/fixtures/error.json new file mode 100644 index 000000000..8ddf8df25 --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/error.json @@ -0,0 +1,24 @@ +{ + "result": null, + "error": { + "filename": "service", + "lineno": 13, + "code": "Server.Keyfailure", + "string": "Unknown keyname user", + "detail": [ + "Traceback (most recent call last):", + " File \"/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py\", line 159, in dispatch_request", + " result = self.call_method(method,req_dict,tc,export_dict,log_line)", + " File \"/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py\", line 96, in call_method", + " result = getattr(service_class_instance,req_dict['methodname'])(*args)", + " File \"/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/ladonizer/decorator.py\", line 77, in injector", + " res = f(*args,**kw)", + " File \"/usr/local/namesurfer/webui2/webui/service/service10/NSService_10.py\", line 502, in addDNSRecord", + " key = validate_key(keyname, digest, [zonename, viewname, record.name, record.type, str(record.ttl), record.data])", + " File \"/usr/local/namesurfer/webui2/webui/service/base/implementation.py\", line 63, in validate_key", + " raise ApiFault('Server.Keyfailure', 'Unknown keyname %s' % keyname)", + "ApiFault: service(13): Unknown keyname user", + "" + ] + } +} diff --git a/providers/dns/namesurfer/internal/fixtures/listZones-request.json b/providers/dns/namesurfer/internal/fixtures/listZones-request.json new file mode 100644 index 000000000..06689de7a --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/listZones-request.json @@ -0,0 +1,9 @@ +{ + "id": 1, + "method": "listZones", + "params": [ + "user", + "2739461ea1a3dc51302993f724f40228409c53b78025d8d7b1d7bba3c1bf2d66", + "value" + ] +} diff --git a/providers/dns/namesurfer/internal/fixtures/listZones.json b/providers/dns/namesurfer/internal/fixtures/listZones.json new file mode 100644 index 000000000..37fa2053b --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/listZones.json @@ -0,0 +1,17 @@ +{ + "id": 1, + "result": [ + { + "name": "example.com", + "view": "viewA" + }, + { + "name": "example.org", + "view": "viewB" + }, + { + "name": "example.net", + "view": "viewC" + } + ] +} diff --git a/providers/dns/namesurfer/internal/fixtures/searchDNSHosts-request.json b/providers/dns/namesurfer/internal/fixtures/searchDNSHosts-request.json new file mode 100644 index 000000000..4a88340e2 --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/searchDNSHosts-request.json @@ -0,0 +1,9 @@ +{ + "id": 1, + "method": "searchDNSHosts", + "params": [ + "user", + "02cf1a2f6e124507d16738d595f583932185313fc96afc2d8404960acaec29b4", + "value" + ] +} diff --git a/providers/dns/namesurfer/internal/fixtures/searchDNSHosts.json b/providers/dns/namesurfer/internal/fixtures/searchDNSHosts.json new file mode 100644 index 000000000..822459148 --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/searchDNSHosts.json @@ -0,0 +1,23 @@ +{ + "id": 1, + "result": [ + { + "name": "foo", + "type": "TXT", + "data": "xxx", + "ttl": 300 + }, + { + "name": "_acme-challenge", + "type": "TXT", + "data": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 300 + }, + { + "name": "bar", + "type": "A", + "data": "yyy", + "ttl": 300 + } + ] +} diff --git a/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json b/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json new file mode 100644 index 000000000..494de20c6 --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json @@ -0,0 +1,22 @@ +{ + "id": 1, + "method": "updateDNSHost", + "params": [ + "user", + "510e63288ac874c1d5ba313a9411591daa346e5621fb0153263adc278794e378", + "example.com", + "viewA", + { + "name": "_acme-challenge", + "type": "TXT", + "data": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 300 + }, + { + "name": "", + "type": "", + "data": "", + "ttl": 0 + } + ] +} diff --git a/providers/dns/namesurfer/internal/fixtures/updateDNSHost.json b/providers/dns/namesurfer/internal/fixtures/updateDNSHost.json new file mode 100644 index 000000000..f41779e30 --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/updateDNSHost.json @@ -0,0 +1,4 @@ +{ + "id": 1, + "result": true +} diff --git a/providers/dns/namesurfer/internal/types.go b/providers/dns/namesurfer/internal/types.go new file mode 100644 index 000000000..d364c1876 --- /dev/null +++ b/providers/dns/namesurfer/internal/types.go @@ -0,0 +1,72 @@ +package internal + +import ( + "encoding/json" + "fmt" + "strings" +) + +// DNSNode represents a DNS record. +// http://95.128.3.201:8053/API/NSService_10#DNSNode +type DNSNode struct { + Name string `json:"name"` + Type string `json:"type"` + Data string `json:"data"` + TTL int `json:"ttl"` +} + +// DNSZone represents a DNS zone. +// http://95.128.3.201:8053/API/NSService_10#DNSZone +type DNSZone struct { + Name string `json:"name,omitempty"` + View string `json:"view,omitempty"` +} + +// APIRequest represents a JSON-RPC request. +// https://www.jsonrpc.org/specification_v1#a1.1Requestmethodinvocation +type APIRequest struct { + ID any `json:"id"` // Can be int or string depending on API + Method string `json:"method"` + Params []any `json:"params"` +} + +// APIResponse represents a JSON-RPC response. +// https://www.jsonrpc.org/specification_v1#a1.2Response +type APIResponse struct { + ID any `json:"id"` // Can be int or string depending on API + Result json.RawMessage `json:"result"` + Error *APIError `json:"error"` +} + +// APIError represents an error. +type APIError struct { + Code any `json:"code"` // Can be int or string depending on API + Filename string `json:"filename"` + LineNumber int `json:"lineno"` + Message string `json:"string"` + Detail []string `json:"detail"` +} + +func (e *APIError) Error() string { + msg := new(strings.Builder) + + _, _ = fmt.Fprintf(msg, "code: %v", e.Code) + + if e.Filename != "" { + _, _ = fmt.Fprintf(msg, ", filename: %s", e.Filename) + } + + if e.LineNumber > 0 { + _, _ = fmt.Fprintf(msg, ", line: %d", e.LineNumber) + } + + if e.Message != "" { + _, _ = fmt.Fprintf(msg, ", message: %s", e.Message) + } + + if len(e.Detail) > 0 { + _, _ = fmt.Fprintf(msg, ", detail: %v", strings.Join(e.Detail, " ")) + } + + return msg.String() +} diff --git a/providers/dns/namesurfer/namesurfer.go b/providers/dns/namesurfer/namesurfer.go new file mode 100644 index 000000000..6b7f48402 --- /dev/null +++ b/providers/dns/namesurfer/namesurfer.go @@ -0,0 +1,214 @@ +// Package namesurfer implements a DNS provider for solving the DNS-01 challenge using FusionLayer NameSurfer API. +package namesurfer + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" + "github.com/go-acme/lego/v4/providers/dns/namesurfer/internal" +) + +// Environment variables names. +const ( + envNamespace = "NAMESURFER_" + + EnvBaseURL = envNamespace + "BASE_URL" + EnvAPIKey = envNamespace + "API_KEY" + EnvAPISecret = envNamespace + "API_SECRET" + EnvView = envNamespace + "VIEW" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" + EnvInsecureSkipVerify = envNamespace + "INSECURE_SKIP_VERIFY" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + BaseURL string + APIKey string + APISecret string + View 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, 300), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*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 + + zones map[string]string + zonesMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for FusionLayer NameSurfer. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvBaseURL, EnvAPIKey, EnvAPISecret) + if err != nil { + return nil, fmt.Errorf("namesurfer: %w", err) + } + + config := NewDefaultConfig() + config.BaseURL = values[EnvBaseURL] + config.APIKey = values[EnvAPIKey] + config.APISecret = values[EnvAPISecret] + config.View = env.GetOrDefaultString(EnvView, "") + + if env.GetOrDefaultBool(EnvInsecureSkipVerify, false) { + config.HTTPClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for FusionLayer NameSurfer. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("namesurfer: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.BaseURL, config.APIKey, config.APISecret) + if err != nil { + return nil, fmt.Errorf("namesurfer: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + zones: make(map[string]string), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("namesurfer: %w", err) + } + + d.zonesMu.Lock() + d.zones[token] = zone + d.zonesMu.Unlock() + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) + if err != nil { + return fmt.Errorf("namesurfer: %w", err) + } + + record := internal.DNSNode{ + Name: subDomain, + Type: "TXT", + TTL: d.config.TTL, + Data: info.Value, + } + + err = d.client.AddDNSRecord(ctx, zone, d.config.View, record) + if err != nil { + return fmt.Errorf("namesurfer: add DNS record: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + d.zonesMu.Lock() + zone, ok := d.zones[token] + d.zonesMu.Unlock() + + if !ok { + return fmt.Errorf("namesurfer: unknown zone for '%s'", info.EffectiveFQDN) + } + + d.zonesMu.Lock() + delete(d.zones, token) + d.zonesMu.Unlock() + + existing, err := d.client.SearchDNSHosts(ctx, dns01.UnFqdn(info.EffectiveFQDN)) + if err != nil { + return fmt.Errorf("namesurfer: search DNS hosts: %w", err) + } + + for _, node := range existing { + if node.Type != "TXT" || node.Data != info.Value { + continue + } + + err = d.client.UpdateDNSHost(ctx, zone, d.config.View, node, internal.DNSNode{}) + if err != nil { + return fmt.Errorf("namesurfer: update DNS host: %w", err) + } + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +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) { + zones, err := d.client.ListZones(ctx, "forward") + if err != nil { + return "", fmt.Errorf("list zones: %w", err) + } + + domain := dns01.UnFqdn(fqdn) + + var zoneName string + + for _, zone := range zones { + if strings.HasSuffix(domain, zone.Name) && len(zone.Name) > len(zoneName) { + zoneName = zone.Name + } + } + + if zoneName == "" { + return "", fmt.Errorf("no zone found for %s", fqdn) + } + + return zoneName, nil +} diff --git a/providers/dns/namesurfer/namesurfer.toml b/providers/dns/namesurfer/namesurfer.toml new file mode 100644 index 000000000..fd914ec0c --- /dev/null +++ b/providers/dns/namesurfer/namesurfer.toml @@ -0,0 +1,28 @@ +Name = "FusionLayer NameSurfer" +Description = '''''' +URL = "https://www.fusionlayer.com/" +Code = "namesurfer" +Since = "v4.32.0" + +Example = ''' +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 +''' + +[Configuration] + [Configuration.Credentials] + NAMESURFER_BASE_URL = "The base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10)" + NAMESURFER_API_KEY = "API key name" + NAMESURFER_API_SECRET = "API secret" + [Configuration.Additional] + NAMESURFER_VIEW = "DNS view name (optional, default: empty string)" + 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_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + NAMESURFER_INSECURE_SKIP_VERIFY = "Whether to verify the API certificate" + +[Links] + API = "https://web.archive.org/web/20260213170737/http://95.128.3.201:8053/API/NSService_10" diff --git a/providers/dns/namesurfer/namesurfer_test.go b/providers/dns/namesurfer/namesurfer_test.go new file mode 100644 index 000000000..ce3aa37af --- /dev/null +++ b/providers/dns/namesurfer/namesurfer_test.go @@ -0,0 +1,174 @@ +package namesurfer + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvBaseURL, + EnvAPIKey, + EnvAPISecret, + EnvView, +).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvBaseURL: "https://example.com", + EnvAPIKey: "user", + EnvAPISecret: "secret", + }, + }, + { + desc: "missing base URL", + envVars: map[string]string{ + EnvBaseURL: "", + EnvAPIKey: "user", + EnvAPISecret: "secret", + }, + expected: "namesurfer: some credentials information are missing: NAMESURFER_BASE_URL", + }, + { + desc: "missing API key", + envVars: map[string]string{ + EnvBaseURL: "https://example.com", + EnvAPIKey: "", + EnvAPISecret: "secret", + }, + expected: "namesurfer: some credentials information are missing: NAMESURFER_API_KEY", + }, + { + desc: "missing API secret", + envVars: map[string]string{ + EnvBaseURL: "https://example.com", + EnvAPIKey: "user", + EnvAPISecret: "", + }, + expected: "namesurfer: some credentials information are missing: NAMESURFER_API_SECRET", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "namesurfer: some credentials information are missing: NAMESURFER_BASE_URL,NAMESURFER_API_KEY,NAMESURFER_API_SECRET", + }, + } + + 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 + baseURL string + apiKey string + apiSecret string + expected string + }{ + { + desc: "success", + baseURL: "https://example.com", + apiKey: "user", + apiSecret: "secret", + }, + { + desc: "missing base URL", + apiKey: "user", + apiSecret: "secret", + expected: "namesurfer: base URL missing", + }, + { + desc: "missing API key", + baseURL: "https://example.com", + apiSecret: "secret", + expected: "namesurfer: credentials missing", + }, + { + desc: "missing API secret", + baseURL: "https://example.com", + apiKey: "user", + expected: "namesurfer: credentials missing", + }, + { + desc: "missing credentials", + expected: "namesurfer: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.BaseURL = test.baseURL + config.APIKey = test.apiKey + config.APISecret = test.apiSecret + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/nearlyfreespeech/nearlyfreespeech.toml b/providers/dns/nearlyfreespeech/nearlyfreespeech.toml index 80d4fd6bc..3a1e25942 100644 --- a/providers/dns/nearlyfreespeech/nearlyfreespeech.toml +++ b/providers/dns/nearlyfreespeech/nearlyfreespeech.toml @@ -7,7 +7,7 @@ Since = "v4.8.0" Example = ''' NEARLYFREESPEECH_API_KEY=xxxxxx \ NEARLYFREESPEECH_LOGIN=xxxx \ -lego --email you@example.com --dns nearlyfreespeech -d '*.example.com' -d example.com run +lego --dns nearlyfreespeech -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/neodigit/neodigit.go b/providers/dns/neodigit/neodigit.go index eb4530479..d41846307 100644 --- a/providers/dns/neodigit/neodigit.go +++ b/providers/dns/neodigit/neodigit.go @@ -25,6 +25,8 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) +const defaultBaseURL = "https://api.neodigit.net/v1" + var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. @@ -66,7 +68,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("neodigit: the configuration of the DNS provider is nil") } - provider, err := tecnocratica.NewDNSProviderConfig(config, "") + provider, err := tecnocratica.NewDNSProviderConfig(config, defaultBaseURL) if err != nil { return nil, fmt.Errorf("neodigit: %w", err) } diff --git a/providers/dns/neodigit/neodigit.toml b/providers/dns/neodigit/neodigit.toml index b391a6512..91b3cfb07 100644 --- a/providers/dns/neodigit/neodigit.toml +++ b/providers/dns/neodigit/neodigit.toml @@ -6,7 +6,7 @@ Since = "v4.30.0" Example = ''' NEODIGIT_TOKEN=xxxxxx \ -lego --email you@example.com --dns neodigit -d '*.example.com' -d example.com run +lego --dns neodigit -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/netcup/netcup.toml b/providers/dns/netcup/netcup.toml index 0df09b0df..4ef8688c6 100644 --- a/providers/dns/netcup/netcup.toml +++ b/providers/dns/netcup/netcup.toml @@ -8,7 +8,7 @@ Example = ''' NETCUP_CUSTOMER_NUMBER=xxxx \ NETCUP_API_KEY=yyyy \ NETCUP_API_PASSWORD=zzzz \ -lego --email you@example.com --dns netcup -d '*.example.com' -d example.com run +lego --dns netcup -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/netlify/netlify.toml b/providers/dns/netlify/netlify.toml index c5cb670f9..9d3c0f6b5 100644 --- a/providers/dns/netlify/netlify.toml +++ b/providers/dns/netlify/netlify.toml @@ -6,7 +6,7 @@ Since = "v3.7.0" Example = ''' NETLIFY_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns netlify -d '*.example.com' -d example.com run +lego --dns netlify -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/nicmanager/nicmanager.toml b/providers/dns/nicmanager/nicmanager.toml index 7fdf296c4..d5921de5a 100644 --- a/providers/dns/nicmanager/nicmanager.toml +++ b/providers/dns/nicmanager/nicmanager.toml @@ -13,7 +13,7 @@ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ -lego --email you@example.com --dns nicmanager -d '*.example.com' -d example.com run +lego --dns nicmanager -d '*.example.com' -d example.com run ## Login using account name + username @@ -24,7 +24,7 @@ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ -lego --email you@example.com --dns nicmanager -d '*.example.com' -d example.com run +lego --dns nicmanager -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/nicru/nicru.toml b/providers/dns/nicru/nicru.toml index 6bffe74a5..f955511a2 100644 --- a/providers/dns/nicru/nicru.toml +++ b/providers/dns/nicru/nicru.toml @@ -9,7 +9,7 @@ NICRU_USER="" \ NICRU_PASSWORD="" \ NICRU_SERVICE_ID="" \ NICRU_SECRET="" \ -lego --dns nicru --domains "*.example.com" --email you@example.com run +lego --dns nicru -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/nifcloud/nifcloud.toml b/providers/dns/nifcloud/nifcloud.toml index b692bb9d3..3c43b1dc0 100644 --- a/providers/dns/nifcloud/nifcloud.toml +++ b/providers/dns/nifcloud/nifcloud.toml @@ -7,7 +7,7 @@ Since = "v1.1.0" Example = ''' NIFCLOUD_ACCESS_KEY_ID=xxxx \ NIFCLOUD_SECRET_ACCESS_KEY=yyyy \ -lego --email you@example.com --dns nifcloud -d '*.example.com' -d example.com run +lego --dns nifcloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/njalla/njalla.toml b/providers/dns/njalla/njalla.toml index ef1fe158e..ff4750b7d 100644 --- a/providers/dns/njalla/njalla.toml +++ b/providers/dns/njalla/njalla.toml @@ -6,7 +6,7 @@ Since = "v4.3.0" Example = ''' NJALLA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns njalla -d '*.example.com' -d example.com run +lego --dns njalla -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/nodion/nodion.toml b/providers/dns/nodion/nodion.toml index 0888f96c3..c9db46e61 100644 --- a/providers/dns/nodion/nodion.toml +++ b/providers/dns/nodion/nodion.toml @@ -6,7 +6,7 @@ Since = "v4.11.0" Example = ''' NODION_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns nodion -d '*.example.com' -d example.com run +lego --dns nodion -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ns1/ns1.toml b/providers/dns/ns1/ns1.toml index 2a6b10deb..829663bf5 100644 --- a/providers/dns/ns1/ns1.toml +++ b/providers/dns/ns1/ns1.toml @@ -6,7 +6,7 @@ Since = "v0.4.0" Example = ''' NS1_API_KEY=xxxx \ -lego --email you@example.com --dns ns1 -d '*.example.com' -d example.com run +lego --dns ns1 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/octenium/octenium.toml b/providers/dns/octenium/octenium.toml index 5084526fd..e3c9d894f 100644 --- a/providers/dns/octenium/octenium.toml +++ b/providers/dns/octenium/octenium.toml @@ -6,7 +6,7 @@ Since = "v4.27.0" Example = ''' OCTENIUM_API_KEY="xxx" \ -lego --email you@example.com --dns octenium -d '*.example.com' -d example.com run +lego --dns octenium -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/oraclecloud/oraclecloud.toml b/providers/dns/oraclecloud/oraclecloud.toml index f13cb1e1e..f6155052e 100644 --- a/providers/dns/oraclecloud/oraclecloud.toml +++ b/providers/dns/oraclecloud/oraclecloud.toml @@ -13,13 +13,13 @@ OCI_USER_OCID="ocid1.user.oc1..secret" \ OCI_FINGERPRINT="00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" \ OCI_REGION="us-phoenix-1" \ OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ -lego --email you@example.com --dns oraclecloud -d '*.example.com' -d example.com run +lego --dns oraclecloud -d '*.example.com' -d example.com run # Using Instance Principal authentication (when running on OCI compute instances): # https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm OCI_AUTH_TYPE="instance_principal" \ OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ -lego --email you@example.com --dns oraclecloud -d '*.example.com' -d example.com run +lego --dns oraclecloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/otc/otc.toml b/providers/dns/otc/otc.toml index 91f9f5455..e63077fda 100644 --- a/providers/dns/otc/otc.toml +++ b/providers/dns/otc/otc.toml @@ -9,7 +9,7 @@ OTC_DOMAIN_NAME=domain_name \ OTC_USER_NAME=user_name \ OTC_PASSWORD=password \ OTC_PROJECT_NAME=project_name \ -lego --email you@example.com --dns otc -d '*.example.com' -d example.com run +lego --dns otc -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ovh/ovh.toml b/providers/dns/ovh/ovh.toml index 95162185b..abf22bd7a 100644 --- a/providers/dns/ovh/ovh.toml +++ b/providers/dns/ovh/ovh.toml @@ -11,20 +11,20 @@ OVH_APPLICATION_KEY=1234567898765432 \ OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \ OVH_CONSUMER_KEY=256vfsd347245sdfg \ OVH_ENDPOINT=ovh-eu \ -lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run +lego --dns ovh -d '*.example.com' -d example.com run # Or Access Token: OVH_ACCESS_TOKEN=xxx \ OVH_ENDPOINT=ovh-eu \ -lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run +lego --dns ovh -d '*.example.com' -d example.com run # Or OAuth2: OVH_CLIENT_ID=yyy \ OVH_CLIENT_SECRET=xxx \ OVH_ENDPOINT=ovh-eu \ -lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run +lego --dns ovh -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/pdns/pdns.toml b/providers/dns/pdns/pdns.toml index 53b5547b9..a83d80922 100644 --- a/providers/dns/pdns/pdns.toml +++ b/providers/dns/pdns/pdns.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' PDNS_API_URL=http://pdns-server:80/ \ PDNS_API_KEY=xxxx \ -lego --email you@example.com --dns pdns -d '*.example.com' -d example.com run +lego --dns pdns -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/plesk/plesk.toml b/providers/dns/plesk/plesk.toml index 5fb4ce073..0ef89d6b7 100644 --- a/providers/dns/plesk/plesk.toml +++ b/providers/dns/plesk/plesk.toml @@ -8,7 +8,7 @@ Example = ''' PLESK_SERVER_BASE_URL="https://plesk.myserver.com:8443" \ PLESK_USERNAME=xxxxxx \ PLESK_PASSWORD=yyyyyy \ -lego --email you@example.com --dns plesk -d '*.example.com' -d example.com run +lego --dns plesk -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/porkbun/porkbun.toml b/providers/dns/porkbun/porkbun.toml index d7ed3aedc..9ae036da6 100644 --- a/providers/dns/porkbun/porkbun.toml +++ b/providers/dns/porkbun/porkbun.toml @@ -8,7 +8,7 @@ Since = "v4.4.0" Example = ''' PORKBUN_SECRET_API_KEY=xxxxxx \ PORKBUN_API_KEY=yyyyyy \ -lego --email you@example.com --dns porkbun -d '*.example.com' -d example.com run +lego --dns porkbun -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/rackspace/rackspace.toml b/providers/dns/rackspace/rackspace.toml index 7ca2c3b7a..0a4a80ffc 100644 --- a/providers/dns/rackspace/rackspace.toml +++ b/providers/dns/rackspace/rackspace.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' RACKSPACE_USER=xxxx \ RACKSPACE_API_KEY=yyyy \ -lego --email you@example.com --dns rackspace -d '*.example.com' -d example.com run +lego --dns rackspace -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/rainyun/rainyun.toml b/providers/dns/rainyun/rainyun.toml index cca16cffe..fe2b3c07d 100644 --- a/providers/dns/rainyun/rainyun.toml +++ b/providers/dns/rainyun/rainyun.toml @@ -6,7 +6,7 @@ Since = "v4.21.0" Example = ''' RAINYUN_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns rainyun -d '*.example.com' -d example.com run +lego --dns rainyun -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/rcodezero/rcodezero.toml b/providers/dns/rcodezero/rcodezero.toml index bba5588da..c2a4a1e7b 100644 --- a/providers/dns/rcodezero/rcodezero.toml +++ b/providers/dns/rcodezero/rcodezero.toml @@ -6,7 +6,7 @@ Since = "v4.13" Example = ''' RCODEZERO_API_TOKEN= \ -lego --email you@example.com --dns rcodezero -d '*.example.com' -d example.com run +lego --dns rcodezero -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/regfish/regfish.toml b/providers/dns/regfish/regfish.toml index 9869ed96e..fbaacbde4 100644 --- a/providers/dns/regfish/regfish.toml +++ b/providers/dns/regfish/regfish.toml @@ -6,7 +6,7 @@ Since = "v4.20.0" Example = ''' REGFISH_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns regfish -d '*.example.com' -d example.com run +lego --dns regfish -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/regru/regru.toml b/providers/dns/regru/regru.toml index 2ccf3a58f..728bb2bf7 100644 --- a/providers/dns/regru/regru.toml +++ b/providers/dns/regru/regru.toml @@ -7,7 +7,7 @@ Since = "v3.5.0" Example = ''' REGRU_USERNAME=xxxxxx \ REGRU_PASSWORD=yyyyyy \ -lego --email you@example.com --dns regru -d '*.example.com' -d example.com run +lego --dns regru -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/rfc2136/rfc2136.toml b/providers/dns/rfc2136/rfc2136.toml index 9243440a4..6b5bbe599 100644 --- a/providers/dns/rfc2136/rfc2136.toml +++ b/providers/dns/rfc2136/rfc2136.toml @@ -9,7 +9,7 @@ RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_KEY=example.com \ RFC2136_TSIG_ALGORITHM=hmac-sha256. \ RFC2136_TSIG_SECRET=YWJjZGVmZGdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNDU= \ -lego --email you@example.com --dns rfc2136 -d '*.example.com' -d example.com run +lego --dns rfc2136 -d '*.example.com' -d example.com run ## --- @@ -17,7 +17,7 @@ keyname=example.com; keyfile=example.com.key; tsig-keygen $keyname > $keyfile RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_FILE="$keyfile" \ -lego --email you@example.com --dns rfc2136 -d '*.example.com' -d example.com run +lego --dns rfc2136 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/rimuhosting/rimuhosting.toml b/providers/dns/rimuhosting/rimuhosting.toml index 0a4f983e2..c1994e2cc 100644 --- a/providers/dns/rimuhosting/rimuhosting.toml +++ b/providers/dns/rimuhosting/rimuhosting.toml @@ -6,7 +6,7 @@ Since = "v0.3.5" Example = ''' RIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns rimuhosting -d '*.example.com' -d example.com run +lego --dns rimuhosting -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/route53/route53.toml b/providers/dns/route53/route53.toml index 9e3b049a6..607d9ef31 100644 --- a/providers/dns/route53/route53.toml +++ b/providers/dns/route53/route53.toml @@ -9,7 +9,7 @@ AWS_ACCESS_KEY_ID=your_key_id \ AWS_SECRET_ACCESS_KEY=your_secret_access_key \ AWS_REGION=aws-region \ AWS_HOSTED_ZONE_ID=your_hosted_zone_id \ -lego --email you@example.com --dns route53 -d '*.example.com' -d example.com run +lego --dns route53 -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/safedns/internal/client.go b/providers/dns/safedns/internal/client.go index 51b12e99d..628618032 100644 --- a/providers/dns/safedns/internal/client.go +++ b/providers/dns/safedns/internal/client.go @@ -19,7 +19,7 @@ const defaultBaseURL = "https://api.ukfast.io/safedns/v1" const authorizationHeader = "Authorization" -// Client the UKFast SafeDNS client. +// Client the ANS SafeDNS client. type Client struct { authToken string diff --git a/providers/dns/safedns/safedns.go b/providers/dns/safedns/safedns.go index be8ca4fe6..154cfc5ee 100644 --- a/providers/dns/safedns/safedns.go +++ b/providers/dns/safedns/safedns.go @@ -1,4 +1,4 @@ -// Package safedns implements a DNS provider for solving the DNS-01 challenge using UKFast SafeDNS. +// Package safedns implements a DNS provider for solving the DNS-01 challenge using ANS SafeDNS. package safedns import ( @@ -75,7 +75,7 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderConfig return a DNSProvider instance configured for UKFast SafeDNS. +// NewDNSProviderConfig return a DNSProvider instance configured for ANS SafeDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("safedns: supplied configuration was nil") diff --git a/providers/dns/safedns/safedns.toml b/providers/dns/safedns/safedns.toml index dcc7bc90e..f387f2535 100644 --- a/providers/dns/safedns/safedns.toml +++ b/providers/dns/safedns/safedns.toml @@ -1,12 +1,12 @@ -Name = "UKFast SafeDNS" +Name = "ANS SafeDNS" Description = '''''' -URL = "https://www.ukfast.co.uk/dns-hosting.html" +URL = "https://www.ans.co.uk/" Code = "safedns" Since = "v4.6.0" Example = ''' SAFEDNS_AUTH_TOKEN=xxxxxx \ -lego --email you@example.com --dns safedns -d '*.example.com' -d example.com run +lego --dns safedns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/sakuracloud/sakuracloud.toml b/providers/dns/sakuracloud/sakuracloud.toml index f754e0c89..a197cd27c 100644 --- a/providers/dns/sakuracloud/sakuracloud.toml +++ b/providers/dns/sakuracloud/sakuracloud.toml @@ -7,7 +7,7 @@ Since = "v1.1.0" Example = ''' SAKURACLOUD_ACCESS_TOKEN=xxxxx \ SAKURACLOUD_ACCESS_TOKEN_SECRET=yyyyy \ -lego --email you@example.com --dns sakuracloud -d '*.example.com' -d example.com run +lego --dns sakuracloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/scaleway/scaleway.toml b/providers/dns/scaleway/scaleway.toml index 212cea295..8b556e8b1 100644 --- a/providers/dns/scaleway/scaleway.toml +++ b/providers/dns/scaleway/scaleway.toml @@ -6,7 +6,7 @@ Since = "v3.4.0" Example = ''' SCW_SECRET_KEY=xxxxxxx-xxxxx-xxxx-xxx-xxxxxx \ -lego --email you@example.com --dns scaleway -d '*.example.com' -d example.com run +lego --dns scaleway -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/selectel/selectel.toml b/providers/dns/selectel/selectel.toml index f9add7ea9..087c97b5b 100644 --- a/providers/dns/selectel/selectel.toml +++ b/providers/dns/selectel/selectel.toml @@ -6,7 +6,7 @@ Since = "v1.2.0" Example = ''' SELECTEL_API_TOKEN=xxxxx \ -lego --email you@example.com --dns selectel -d '*.example.com' -d example.com run +lego --dns selectel -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/selectelv2/selectelv2.go b/providers/dns/selectelv2/selectelv2.go index 6e3c1f42c..1fcb48583 100644 --- a/providers/dns/selectelv2/selectelv2.go +++ b/providers/dns/selectelv2/selectelv2.go @@ -297,10 +297,10 @@ func (w *clientWrapper) getZone(ctx context.Context, name string) (*selectelapi. return nil, fmt.Errorf("zone '%s' for challenge has not been found", name) } - // -1 can not be returned since if no dots present we exit above - i := strings.Index(name, ".") + // after is always defined since if no dots present we exit above. + _, after, _ := strings.Cut(name, ".") - return w.getZone(ctx, name[i+1:]) + return w.getZone(ctx, after) } func (w *clientWrapper) getRRset(ctx context.Context, name, zoneID string) (*selectelapi.RRSet, error) { diff --git a/providers/dns/selectelv2/selectelv2.toml b/providers/dns/selectelv2/selectelv2.toml index fd8dbda9f..480c7756e 100644 --- a/providers/dns/selectelv2/selectelv2.toml +++ b/providers/dns/selectelv2/selectelv2.toml @@ -9,7 +9,7 @@ SELECTELV2_USERNAME=trex \ SELECTELV2_PASSWORD=xxxxx \ SELECTELV2_ACCOUNT_ID=1234567 \ SELECTELV2_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \ -lego --email you@example.com --dns selectelv2 -d '*.example.com' -d example.com run +lego --dns selectelv2 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/selfhostde/selfhostde.toml b/providers/dns/selfhostde/selfhostde.toml index 619f2cae8..bd22c6c41 100644 --- a/providers/dns/selfhostde/selfhostde.toml +++ b/providers/dns/selfhostde/selfhostde.toml @@ -8,7 +8,7 @@ Example = ''' SELFHOSTDE_USERNAME=xxx \ SELFHOSTDE_PASSWORD=yyy \ SELFHOSTDE_RECORDS_MAPPING=my.example.com:123 \ -lego --email you@example.com --dns selfhostde -d '*.example.com' -d example.com run +lego --dns selfhostde -d '*.example.com' -d example.com run ''' Additional = """ diff --git a/providers/dns/servercow/servercow.toml b/providers/dns/servercow/servercow.toml index de9727163..5cbacbb88 100644 --- a/providers/dns/servercow/servercow.toml +++ b/providers/dns/servercow/servercow.toml @@ -7,7 +7,7 @@ Since = "v3.4.0" Example = ''' SERVERCOW_USERNAME=xxxxxxxx \ SERVERCOW_PASSWORD=xxxxxxxx \ -lego --email you@example.com --dns servercow -d '*.example.com' -d example.com run +lego --dns servercow -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/shellrent/shellrent.toml b/providers/dns/shellrent/shellrent.toml index 48a5b9ad9..05b6517fc 100644 --- a/providers/dns/shellrent/shellrent.toml +++ b/providers/dns/shellrent/shellrent.toml @@ -7,7 +7,7 @@ Since = "v4.16.0" Example = ''' SHELLRENT_USERNAME=xxxx \ SHELLRENT_TOKEN=yyyy \ -lego --email you@example.com --dns shellrent -d '*.example.com' -d example.com run +lego --dns shellrent -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/simply/simply.toml b/providers/dns/simply/simply.toml index c586e0db5..a838e245a 100644 --- a/providers/dns/simply/simply.toml +++ b/providers/dns/simply/simply.toml @@ -7,7 +7,7 @@ Since = "v4.4.0" Example = ''' SIMPLY_ACCOUNT_NAME=xxxxxx \ SIMPLY_API_KEY=yyyyyy \ -lego --email you@example.com --dns simply -d '*.example.com' -d example.com run +lego --dns simply -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/sonic/sonic.toml b/providers/dns/sonic/sonic.toml index 921fe4988..cb501e923 100644 --- a/providers/dns/sonic/sonic.toml +++ b/providers/dns/sonic/sonic.toml @@ -7,7 +7,7 @@ Since = "v4.4.0" Example = ''' SONIC_USER_ID=12345 \ SONIC_API_KEY=4d6fbf2f9ab0fa11697470918d37625851fc0c51 \ -lego --email you@example.com --dns sonic -d '*.example.com' -d example.com run +lego --dns sonic -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/spaceship/spaceship.toml b/providers/dns/spaceship/spaceship.toml index 645abd171..e9abcd408 100644 --- a/providers/dns/spaceship/spaceship.toml +++ b/providers/dns/spaceship/spaceship.toml @@ -7,7 +7,7 @@ Since = "v4.22.0" Example = ''' SPACESHIP_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ SPACESHIP_API_SECRET="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns spaceship -d '*.example.com' -d example.com run +lego --dns spaceship -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/stackpath/stackpath.toml b/providers/dns/stackpath/stackpath.toml index cc14cdfba..b50e7035f 100644 --- a/providers/dns/stackpath/stackpath.toml +++ b/providers/dns/stackpath/stackpath.toml @@ -8,7 +8,7 @@ Example = ''' STACKPATH_CLIENT_ID=xxxxx \ STACKPATH_CLIENT_SECRET=yyyyy \ STACKPATH_STACK_ID=zzzzz \ -lego --email you@example.com --dns stackpath -d '*.example.com' -d example.com run +lego --dns stackpath -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/syse/syse.toml b/providers/dns/syse/syse.toml index 0ae585854..b5b1fdf47 100644 --- a/providers/dns/syse/syse.toml +++ b/providers/dns/syse/syse.toml @@ -6,10 +6,10 @@ Since = "v4.30.0" Example = ''' SYSE_CREDENTIALS=example.com:password \ -lego --email you@example.com --dns syse -d '*.example.com' -d example.com run +lego --dns syse -d '*.example.com' -d example.com run SYSE_CREDENTIALS=example.org:password1,example.com:password2 \ -lego --email you@example.com --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com +lego --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com ''' [Configuration] diff --git a/providers/dns/technitium/technitium.toml b/providers/dns/technitium/technitium.toml index 13b40c304..ac1fc6466 100644 --- a/providers/dns/technitium/technitium.toml +++ b/providers/dns/technitium/technitium.toml @@ -7,7 +7,7 @@ Since = "v4.20.0" Example = ''' TECHNITIUM_SERVER_BASE_URL="https://localhost:5380" \ TECHNITIUM_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns technitium -d '*.example.com' -d example.com run +lego --dns technitium -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/tencentcloud/tencentcloud.toml b/providers/dns/tencentcloud/tencentcloud.toml index 7f06d9386..50f4ee9d5 100644 --- a/providers/dns/tencentcloud/tencentcloud.toml +++ b/providers/dns/tencentcloud/tencentcloud.toml @@ -7,7 +7,7 @@ Since = "v4.6.0" Example = ''' TENCENTCLOUD_SECRET_ID=abcdefghijklmnopqrstuvwx \ TENCENTCLOUD_SECRET_KEY=your-secret-key \ -lego --email you@example.com --dns tencentcloud -d '*.example.com' -d example.com run +lego --dns tencentcloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/timewebcloud/internal/types.go b/providers/dns/timewebcloud/internal/types.go index 81da4df5c..80cdb2c70 100644 --- a/providers/dns/timewebcloud/internal/types.go +++ b/providers/dns/timewebcloud/internal/types.go @@ -3,9 +3,11 @@ package internal import "fmt" type DNSRecord struct { - ID int `json:"id,omitempty"` - Type string `json:"type,omitempty"` - Value string `json:"value,omitempty"` + ID int `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Value string `json:"value,omitempty"` + + // SubDomain is the full name of a subdomain (not only the subdomain label). SubDomain string `json:"subdomain,omitempty"` } diff --git a/providers/dns/timewebcloud/timewebcloud.go b/providers/dns/timewebcloud/timewebcloud.go index d71beea4b..a599566e3 100644 --- a/providers/dns/timewebcloud/timewebcloud.go +++ b/providers/dns/timewebcloud/timewebcloud.go @@ -110,15 +110,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("timewebcloud: could not find zone for domain %q: %w", domain, err) } - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("timewebcloud: %w", err) - } - record := internal.DNSRecord{ Type: "TXT", Value: info.Value, - SubDomain: subDomain, + SubDomain: dns01.UnFqdn(info.EffectiveFQDN), } response, err := d.client.CreateRecord(context.Background(), authZone, record) diff --git a/providers/dns/timewebcloud/timewebcloud.toml b/providers/dns/timewebcloud/timewebcloud.toml index 8c20b37b9..c8bde636a 100644 --- a/providers/dns/timewebcloud/timewebcloud.toml +++ b/providers/dns/timewebcloud/timewebcloud.toml @@ -6,7 +6,7 @@ Since = "v4.20.0" Example = ''' TIMEWEBCLOUD_AUTH_TOKEN=xxxxxx \ -lego --email you@example.com --dns timewebcloud -d '*.example.com' -d example.com run +lego --dns timewebcloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/todaynic/internal/client.go b/providers/dns/todaynic/internal/client.go new file mode 100644 index 000000000..2c537f4a7 --- /dev/null +++ b/providers/dns/todaynic/internal/client.go @@ -0,0 +1,141 @@ +package internal + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" + querystring "github.com/google/go-querystring/query" +) + +const defaultBaseURL = "https://todapi.now.cn:2443" + +// Client the TodayNIC API client. +type Client struct { + authUserID string + apiKey string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(authUserID, apiKey string) (*Client, error) { + if authUserID == "" || apiKey == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + authUserID: authUserID, + apiKey: apiKey, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) AddRecord(ctx context.Context, record Record) (int, error) { + endpoint := c.BaseURL.JoinPath("api", "dns", "add-domain-record.json") + + query, err := querystring.Values(record) + if err != nil { + return 0, err + } + + req, err := c.newRequest(ctx, endpoint, query) + if err != nil { + return 0, err + } + + var result APIResponse + + err = c.do(req, &result) + if err != nil { + return 0, err + } + + return result.ID, nil +} + +func (c *Client) DeleteRecord(ctx context.Context, recordID int) error { + endpoint := c.BaseURL.JoinPath("api", "dns", "delete-domain-record.json") + + query := endpoint.Query() + query.Set("Id", strconv.Itoa(recordID)) + + req, err := c.newRequest(ctx, endpoint, query) + if err != nil { + return err + } + + return c.do(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 (c *Client) newRequest(ctx context.Context, endpoint *url.URL, query url.Values) (*http.Request, error) { + query.Set("auth-userid", c.authUserID) + query.Set("api-key", c.apiKey) + + 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) + } + + req.Header.Set("Accept", "application/json") + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var errAPI APIError + + err := json.Unmarshal(raw, &errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return &errAPI +} diff --git a/providers/dns/todaynic/internal/client_test.go b/providers/dns/todaynic/internal/client_test.go new file mode 100644 index 000000000..71ee7f8b7 --- /dev/null +++ b/providers/dns/todaynic/internal/client_test.go @@ -0,0 +1,94 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("user123", "secret") + if err != nil { + return nil, err + } + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + ) +} + +func TestClient_AddRecord(t *testing.T) { + client := mockBuilder(). + Route("GET /api/dns/add-domain-record.json", + servermock.ResponseFromFixture("add_record.json"), + servermock.CheckQueryParameter().Strict(). + With("Domain", "example.com"). + With("Host", "_acme-challenge"). + With("Type", "TXT"). + With("Value", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + With("Ttl", "600"). + With("auth-userid", "user123"). + With("api-key", "secret"), + ). + Build(t) + + record := Record{ + Domain: "example.com", + Host: "_acme-challenge", + Type: "TXT", + Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: "600", + } + + recordID, err := client.AddRecord(t.Context(), record) + require.NoError(t, err) + + assert.Equal(t, 11554102, recordID) +} + +func TestClient_AddRecord_error(t *testing.T) { + client := mockBuilder(). + Route("GET /api/dns/add-domain-record.json", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusNotFound), + ). + Build(t) + + record := Record{ + Domain: "example.com", + Host: "_acme-challenge", + Type: "TXT", + Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: "600", + } + + _, err := client.AddRecord(t.Context(), record) + require.EqualError(t, err, "host.repeat (2d5876b2-f272-43e9-acc1-4c6a3d3683b1)") +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("GET /api/dns/delete-domain-record.json", + servermock.ResponseFromFixture("add_record.json"), + servermock.CheckQueryParameter().Strict(). + With("Id", "123"). + With("auth-userid", "user123"). + With("api-key", "secret"), + ). + Build(t) + + err := client.DeleteRecord(t.Context(), 123) + require.NoError(t, err) +} diff --git a/providers/dns/todaynic/internal/fixtures/add_record.json b/providers/dns/todaynic/internal/fixtures/add_record.json new file mode 100644 index 000000000..27f34d71c --- /dev/null +++ b/providers/dns/todaynic/internal/fixtures/add_record.json @@ -0,0 +1,4 @@ +{ + "RequestId": "f60ea4d9-67ef-49fa-bbae-06178a6e7293", + "Id": 11554102 +} diff --git a/providers/dns/todaynic/internal/fixtures/error.json b/providers/dns/todaynic/internal/fixtures/error.json new file mode 100644 index 000000000..3ea9c9310 --- /dev/null +++ b/providers/dns/todaynic/internal/fixtures/error.json @@ -0,0 +1,4 @@ +{ + "RequestId": "2d5876b2-f272-43e9-acc1-4c6a3d3683b1", + "error": "host.repeat" +} diff --git a/providers/dns/todaynic/internal/types.go b/providers/dns/todaynic/internal/types.go new file mode 100644 index 000000000..0a15c7da8 --- /dev/null +++ b/providers/dns/todaynic/internal/types.go @@ -0,0 +1,26 @@ +package internal + +import "fmt" + +type APIError struct { + RequestID string `json:"RequestId"` + Message string `json:"error"` +} + +func (a *APIError) Error() string { + return fmt.Sprintf("%s (%s)", a.Message, a.RequestID) +} + +type Record struct { + Domain string `url:"Domain,omitempty"` + Host string `url:"Host,omitempty"` + Type string `url:"Type,omitempty"` + Value string `url:"Value,omitempty"` + Mx string `url:"Mx,omitempty"` + TTL string `url:"Ttl,omitempty"` +} + +type APIResponse struct { + RequestID string `json:"RequestId"` + ID int `json:"Id"` +} diff --git a/providers/dns/todaynic/todaynic.go b/providers/dns/todaynic/todaynic.go new file mode 100644 index 000000000..3a3734033 --- /dev/null +++ b/providers/dns/todaynic/todaynic.go @@ -0,0 +1,164 @@ +// Package todaynic implements a DNS provider for solving the DNS-01 challenge using TodayNIC. +package todaynic + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" + "github.com/go-acme/lego/v4/providers/dns/todaynic/internal" +) + +// Environment variables names. +const ( + envNamespace = "TODAYNIC_" + + EnvAuthUserID = envNamespace + "AUTH_USER_ID" + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + AuthUserID string + APIKey string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 600), + 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 + + recordIDs map[string]int + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for TodayNIC. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAuthUserID, EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("todaynic: %w", err) + } + + config := NewDefaultConfig() + config.AuthUserID = values[EnvAuthUserID] + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for TodayNIC. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("todaynic: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.AuthUserID, config.APIKey) + if err != nil { + return nil, fmt.Errorf("todaynic: %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]int), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("todaynic: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("todaynic: %w", err) + } + + record := internal.Record{ + Domain: dns01.UnFqdn(authZone), + Host: subDomain, + Type: "TXT", + Value: info.Value, + TTL: strconv.Itoa(d.config.TTL), + } + + recordID, err := d.client.AddRecord(context.Background(), record) + if err != nil { + return fmt.Errorf("todaynic: add record: %w", err) + } + + d.recordIDsMu.Lock() + d.recordIDs[token] = recordID + d.recordIDsMu.Unlock() + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + + if !ok { + return fmt.Errorf("todaynic: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + err := d.client.DeleteRecord(context.Background(), recordID) + if err != nil { + return fmt.Errorf("todaynic: delete record: %w", err) + } + + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/dns/todaynic/todaynic.toml b/providers/dns/todaynic/todaynic.toml new file mode 100644 index 000000000..16d55ccc0 --- /dev/null +++ b/providers/dns/todaynic/todaynic.toml @@ -0,0 +1,25 @@ +Name = "TodayNIC/时代互联" +Description = '''''' +URL = "https://www.todaynic.com/" +Code = "todaynic" +Since = "v4.32.0" + +Example = ''' +TODAYNIC_AUTH_USER_ID="xxx" \ +TODAYNIC_API_KEY="yyy" \ +lego --dns todaynic -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + TODAYNIC_AUTH_USER_ID = "account ID" + TODAYNIC_API_KEY = "API key" + [Configuration.Additional] + TODAYNIC_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + TODAYNIC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + TODAYNIC_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" + TODAYNIC_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://www.todaynic.com/partner/mode_Http_Api_detail.php" + apipost = "https://docs.apipost.net/docs/detail/49dcef10a876000?target_id=0" diff --git a/providers/dns/todaynic/todaynic_test.go b/providers/dns/todaynic/todaynic_test.go new file mode 100644 index 000000000..c73bf6cc5 --- /dev/null +++ b/providers/dns/todaynic/todaynic_test.go @@ -0,0 +1,207 @@ +package todaynic + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAuthUserID, EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAuthUserID: "user123", + EnvAPIKey: "secret", + }, + }, + { + desc: "missing user ID", + envVars: map[string]string{ + EnvAuthUserID: "", + EnvAPIKey: "secret", + }, + expected: "todaynic: some credentials information are missing: TODAYNIC_AUTH_USER_ID", + }, + { + desc: "missing API key", + envVars: map[string]string{ + EnvAuthUserID: "user123", + EnvAPIKey: "", + }, + expected: "todaynic: some credentials information are missing: TODAYNIC_API_KEY", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "todaynic: some credentials information are missing: TODAYNIC_AUTH_USER_ID,TODAYNIC_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 + authUserID string + apiKey string + expected string + }{ + { + desc: "success", + authUserID: "user123", + apiKey: "secret", + }, + { + desc: "missing user ID", + apiKey: "secret", + expected: "todaynic: credentials missing", + }, + { + desc: "missing API key", + authUserID: "user123", + expected: "todaynic: credentials missing", + }, + { + desc: "missing credentials", + expected: "todaynic: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.AuthUserID = test.authUserID + config.APIKey = test.apiKey + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.AuthUserID = "user123" + 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(), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /api/dns/add-domain-record.json", + servermock.ResponseFromInternal("add_record.json"), + servermock.CheckQueryParameter().Strict(). + With("Domain", "example.com"). + With("Host", "_acme-challenge"). + With("Type", "TXT"). + With("Value", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + With("Ttl", "600"). + With("auth-userid", "user123"). + With("api-key", "secret"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("GET /api/dns/delete-domain-record.json", + servermock.ResponseFromInternal("add_record.json"), + servermock.CheckQueryParameter().Strict(). + With("Id", "123"). + With("auth-userid", "user123"). + With("api-key", "secret"), + ). + Build(t) + + provider.recordIDs["abc"] = 123 + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/transip/transip.toml b/providers/dns/transip/transip.toml index a894cc3e3..bf7d58ee3 100644 --- a/providers/dns/transip/transip.toml +++ b/providers/dns/transip/transip.toml @@ -7,7 +7,7 @@ Since = "v2.0.0" Example = ''' TRANSIP_ACCOUNT_NAME = "Account name" \ TRANSIP_PRIVATE_KEY_PATH = "transip.key" \ -lego --email you@example.com --dns transip -d '*.example.com' -d example.com run +lego --dns transip -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ultradns/ultradns.toml b/providers/dns/ultradns/ultradns.toml index a403a3dcf..4c3dbbe72 100644 --- a/providers/dns/ultradns/ultradns.toml +++ b/providers/dns/ultradns/ultradns.toml @@ -7,7 +7,7 @@ Since = "v4.10.0" Example = ''' ULTRADNS_USERNAME=username \ ULTRADNS_PASSWORD=password \ -lego --email you@example.com --dns ultradns -d '*.example.com' -d example.com run +lego --dns ultradns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/uniteddomains/uniteddomains.toml b/providers/dns/uniteddomains/uniteddomains.toml index 3663cb867..fe8b9e574 100644 --- a/providers/dns/uniteddomains/uniteddomains.toml +++ b/providers/dns/uniteddomains/uniteddomains.toml @@ -6,7 +6,7 @@ Since = "v4.29.0" Example = ''' UNITEDDOMAINS_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns uniteddomains -d '*.example.com' -d example.com run +lego --dns uniteddomains -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/variomedia/variomedia.toml b/providers/dns/variomedia/variomedia.toml index fe6f2797a..8390d1922 100644 --- a/providers/dns/variomedia/variomedia.toml +++ b/providers/dns/variomedia/variomedia.toml @@ -6,7 +6,7 @@ Since = "v4.8.0" Example = ''' VARIOMEDIA_API_TOKEN=xxxx \ -lego --email you@example.com --dns variomedia -d '*.example.com' -d example.com run +lego --dns variomedia -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/vercel/vercel.toml b/providers/dns/vercel/vercel.toml index 2545b9c48..4700d6d78 100644 --- a/providers/dns/vercel/vercel.toml +++ b/providers/dns/vercel/vercel.toml @@ -6,7 +6,7 @@ Since = "v4.7.0" Example = ''' VERCEL_API_TOKEN=xxxxxx \ -lego --email you@example.com --dns vercel -d '*.example.com' -d example.com run +lego --dns vercel -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/versio/versio.toml b/providers/dns/versio/versio.toml index 33f7125c8..733947095 100644 --- a/providers/dns/versio/versio.toml +++ b/providers/dns/versio/versio.toml @@ -7,7 +7,7 @@ Since = "v2.7.0" Example = ''' VERSIO_USERNAME= \ VERSIO_PASSWORD= \ -lego --email you@example.com --dns versio -d '*.example.com' -d example.com run +lego --dns versio -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/vinyldns/vinyldns.toml b/providers/dns/vinyldns/vinyldns.toml index 5789d10ab..d6dd5810e 100644 --- a/providers/dns/vinyldns/vinyldns.toml +++ b/providers/dns/vinyldns/vinyldns.toml @@ -8,7 +8,7 @@ Example = ''' VINYLDNS_ACCESS_KEY=xxxxxx \ VINYLDNS_SECRET_KEY=yyyyy \ VINYLDNS_HOST=https://api.vinyldns.example.org:9443 \ -lego --email you@example.com --dns vinyldns -d '*.example.com' -d example.com run +lego --dns vinyldns -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/virtualname/virtualname.go b/providers/dns/virtualname/virtualname.go index 6b04e8169..34637d280 100644 --- a/providers/dns/virtualname/virtualname.go +++ b/providers/dns/virtualname/virtualname.go @@ -25,6 +25,8 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) +const defaultBaseURL = "https://api.virtualname.net/v1" + var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. @@ -66,7 +68,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("virtualname: the configuration of the DNS provider is nil") } - provider, err := tecnocratica.NewDNSProviderConfig(config, "https://api.virtualname.net/v1") + provider, err := tecnocratica.NewDNSProviderConfig(config, defaultBaseURL) if err != nil { return nil, fmt.Errorf("virtualname: %w", err) } diff --git a/providers/dns/virtualname/virtualname.toml b/providers/dns/virtualname/virtualname.toml index 7cc4c5344..881f09797 100644 --- a/providers/dns/virtualname/virtualname.toml +++ b/providers/dns/virtualname/virtualname.toml @@ -6,7 +6,7 @@ Since = "v4.30.0" Example = ''' VIRTUALNAME_TOKEN=xxxxxx \ -lego --email you@example.com --dns virtualname -d '*.example.com' -d example.com run +lego --dns virtualname -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/vkcloud/vkcloud.toml b/providers/dns/vkcloud/vkcloud.toml index 366039694..04f57fea3 100644 --- a/providers/dns/vkcloud/vkcloud.toml +++ b/providers/dns/vkcloud/vkcloud.toml @@ -8,7 +8,7 @@ Example = ''' VK_CLOUD_PROJECT_ID="" \ VK_CLOUD_USERNAME="" \ VK_CLOUD_PASSWORD="" \ -lego --email you@example.com --dns vkcloud -d '*.example.com' -d example.com run +lego --dns vkcloud -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/volcengine/volcengine.toml b/providers/dns/volcengine/volcengine.toml index cb7c219f7..ceedcb18a 100644 --- a/providers/dns/volcengine/volcengine.toml +++ b/providers/dns/volcengine/volcengine.toml @@ -7,7 +7,7 @@ Since = "v4.19.0" Example = ''' VOLC_ACCESSKEY=xxx \ VOLC_SECRETKEY=yyy \ -lego --email you@example.com --dns volcengine -d '*.example.com' -d example.com run +lego --dns volcengine -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/vscale/vscale.toml b/providers/dns/vscale/vscale.toml index 78b6c99e5..f7dc0d943 100644 --- a/providers/dns/vscale/vscale.toml +++ b/providers/dns/vscale/vscale.toml @@ -6,7 +6,7 @@ Since = "v2.0.0" Example = ''' VSCALE_API_TOKEN=xxxxx \ -lego --email you@example.com --dns vscale -d '*.example.com' -d example.com run +lego --dns vscale -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/vultr/vultr.toml b/providers/dns/vultr/vultr.toml index 7d31bd52b..78e878bea 100644 --- a/providers/dns/vultr/vultr.toml +++ b/providers/dns/vultr/vultr.toml @@ -6,7 +6,7 @@ Since = "v0.3.1" Example = ''' VULTR_API_KEY=xxxxx \ -lego --email you@example.com --dns vultr -d '*.example.com' -d example.com run +lego --dns vultr -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/webnames/webnames.toml b/providers/dns/webnames/webnames.toml index 04dea25c5..b038deaf5 100644 --- a/providers/dns/webnames/webnames.toml +++ b/providers/dns/webnames/webnames.toml @@ -7,7 +7,7 @@ Since = "v4.15.0" Example = ''' WEBNAMESRU_API_KEY=xxxxxx \ -lego --email you@example.com --dns webnamesru -d '*.example.com' -d example.com run +lego --dns webnamesru -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/webnamesca/webnamesca.toml b/providers/dns/webnamesca/webnamesca.toml index c7d30751b..ab68a04a0 100644 --- a/providers/dns/webnamesca/webnamesca.toml +++ b/providers/dns/webnamesca/webnamesca.toml @@ -7,7 +7,7 @@ Since = "v4.28.0" Example = ''' WEBNAMESCA_API_USER="xxx" \ WEBNAMESCA_API_KEY="yyy" \ -lego --email you@example.com --dns webnamesca -d '*.example.com' -d example.com run +lego --dns webnamesca -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/websupport/websupport.toml b/providers/dns/websupport/websupport.toml index 1f34b431b..4908f0235 100644 --- a/providers/dns/websupport/websupport.toml +++ b/providers/dns/websupport/websupport.toml @@ -7,7 +7,7 @@ Since = "v4.10.0" Example = ''' WEBSUPPORT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ WEBSUPPORT_SECRET="yyyyyyyyyyyyyyyyyyyyy" \ -lego --email you@example.com --dns websupport -d '*.example.com' -d example.com run +lego --dns websupport -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/wedos/wedos.toml b/providers/dns/wedos/wedos.toml index 2ed351a2d..89abfc16c 100644 --- a/providers/dns/wedos/wedos.toml +++ b/providers/dns/wedos/wedos.toml @@ -7,7 +7,7 @@ Since = "v4.4.0" Example = ''' WEDOS_USERNAME=xxxxxxxx \ WEDOS_WAPI_PASSWORD=xxxxxxxx \ -lego --email you@example.com --dns wedos -d '*.example.com' -d example.com run +lego --dns wedos -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/westcn/westcn.go b/providers/dns/westcn/westcn.go index c641f661d..1906f9737 100644 --- a/providers/dns/westcn/westcn.go +++ b/providers/dns/westcn/westcn.go @@ -2,18 +2,14 @@ package westcn import ( - "context" "errors" "fmt" "net/http" - "sync" "time" "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/westcn/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/westcn" ) // Environment variables names. @@ -29,18 +25,12 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) +const defaultBaseURL = "https://api.west.cn/api/v2" + var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. -type Config struct { - Username string - Password string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = westcn.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { @@ -56,11 +46,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *internal.Client - - recordIDs map[string]int - recordIDsMu sync.Mutex + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for West.cn/西部数码. @@ -83,91 +69,36 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("westcn: the configuration of the DNS provider is nil") } - client, err := internal.NewClient(config.Username, config.Password) + provider, err := westcn.NewDNSProviderConfig(config, defaultBaseURL) if err != nil { return nil, fmt.Errorf("westcn: %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]int), - }, nil + return &DNSProvider{prv: provider}, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("westcn: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + err := d.prv.Present(domain, token, keyAuth) if err != nil { return fmt.Errorf("westcn: %w", err) } - record := internal.Record{ - Domain: dns01.UnFqdn(authZone), - Host: subDomain, - Type: "TXT", - Value: info.Value, - TTL: d.config.TTL, - } - - recordID, err := d.client.AddRecord(context.Background(), record) - if err != nil { - return fmt.Errorf("westcn: add record: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = recordID - d.recordIDsMu.Unlock() - return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { - return fmt.Errorf("westcn: could not find zone for domain %q: %w", domain, err) + return fmt.Errorf("westcn: %w", err) } - // gets the record's unique ID - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("westcn: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID) - if err != nil { - return fmt.Errorf("westcn: delete record: %w", err) - } - - // deletes record ID from map - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval + return d.prv.Timeout() } diff --git a/providers/dns/westcn/westcn.toml b/providers/dns/westcn/westcn.toml index acf3a4e49..1b0cb0a7a 100644 --- a/providers/dns/westcn/westcn.toml +++ b/providers/dns/westcn/westcn.toml @@ -7,7 +7,7 @@ Since = "v4.21.0" Example = ''' WESTCN_USERNAME="xxx" \ WESTCN_PASSWORD="yyy" \ -lego --email you@example.com --dns westcn -d '*.example.com' -d example.com run +lego --dns westcn -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/westcn/westcn_test.go b/providers/dns/westcn/westcn_test.go index 36827fd06..a546d518e 100644 --- a/providers/dns/westcn/westcn_test.go +++ b/providers/dns/westcn/westcn_test.go @@ -60,8 +60,7 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) + require.NotNil(t, p.prv) } else { require.EqualError(t, err, test.expected) } @@ -108,8 +107,7 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) + require.NotNil(t, p.prv) } else { require.EqualError(t, err, test.expected) } diff --git a/providers/dns/yandex/yandex.toml b/providers/dns/yandex/yandex.toml index 78da1444d..a36df069e 100644 --- a/providers/dns/yandex/yandex.toml +++ b/providers/dns/yandex/yandex.toml @@ -7,7 +7,7 @@ Since = "v3.7.0" Example = ''' YANDEX_PDD_TOKEN= \ -lego --email you@example.com --dns yandex -d '*.example.com' -d example.com run +lego --dns yandex -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/yandex360/yandex360.toml b/providers/dns/yandex360/yandex360.toml index 69ea02a28..444b1cc38 100644 --- a/providers/dns/yandex360/yandex360.toml +++ b/providers/dns/yandex360/yandex360.toml @@ -8,7 +8,7 @@ Since = "v4.14.0" Example = ''' YANDEX360_OAUTH_TOKEN= \ YANDEX360_ORG_ID= \ -lego --email you@example.com --dns yandex360 -d '*.example.com' -d example.com run +lego --dns yandex360 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/yandexcloud/yandexcloud.toml b/providers/dns/yandexcloud/yandexcloud.toml index a4bf3ebbb..d4b40bb1d 100644 --- a/providers/dns/yandexcloud/yandexcloud.toml +++ b/providers/dns/yandexcloud/yandexcloud.toml @@ -7,7 +7,7 @@ Since = "v4.9.0" Example = ''' YANDEX_CLOUD_IAM_TOKEN= \ YANDEX_CLOUD_FOLDER_ID= \ -lego --email you@example.com --dns yandexcloud -d '*.example.com' -d example.com run +lego --dns yandexcloud -d '*.example.com' -d example.com run # --- @@ -20,7 +20,7 @@ YANDEX_CLOUD_IAM_TOKEN=$(echo '{ \ "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----" \ }' | base64) \ YANDEX_CLOUD_FOLDER_ID= \ -lego --email you@example.com --dns yandexcloud -d '*.example.com' -d example.com run +lego --dns yandexcloud -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/zoneedit/zoneedit.toml b/providers/dns/zoneedit/zoneedit.toml index d3c547c23..cdc53b33a 100644 --- a/providers/dns/zoneedit/zoneedit.toml +++ b/providers/dns/zoneedit/zoneedit.toml @@ -7,7 +7,7 @@ Since = "v4.25.0" Example = ''' ZONEEDIT_USER="xxxxxxxxxxxxxxxxxxxxx" \ ZONEEDIT_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns zoneedit -d '*.example.com' -d example.com run +lego --dns zoneedit -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/zoneee/zoneee.toml b/providers/dns/zoneee/zoneee.toml index 75ccabbda..ab7133180 100644 --- a/providers/dns/zoneee/zoneee.toml +++ b/providers/dns/zoneee/zoneee.toml @@ -7,7 +7,7 @@ Since = "v2.1.0" Example = ''' ZONEEE_API_USER=xxxxx \ ZONEEE_API_KEY=yyyyy \ -lego --email you@example.com --dns zoneee -d '*.example.com' -d example.com run +lego --dns zoneee -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/zonomi/zonomi.toml b/providers/dns/zonomi/zonomi.toml index a5577999a..b91bcaac6 100644 --- a/providers/dns/zonomi/zonomi.toml +++ b/providers/dns/zonomi/zonomi.toml @@ -6,7 +6,7 @@ Since = "v3.5.0" Example = ''' ZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns zonomi -d '*.example.com' -d example.com run +lego --dns zonomi -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 1270e0f9d..9c4bc9e61 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -11,7 +11,9 @@ import ( "github.com/go-acme/lego/v4/providers/dns/alidns" "github.com/go-acme/lego/v4/providers/dns/aliesa" "github.com/go-acme/lego/v4/providers/dns/allinkl" + "github.com/go-acme/lego/v4/providers/dns/alwaysdata" "github.com/go-acme/lego/v4/providers/dns/anexia" + "github.com/go-acme/lego/v4/providers/dns/artfiles" "github.com/go-acme/lego/v4/providers/dns/arvancloud" "github.com/go-acme/lego/v4/providers/dns/auroradns" "github.com/go-acme/lego/v4/providers/dns/autodns" @@ -24,6 +26,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/binarylane" "github.com/go-acme/lego/v4/providers/dns/bindman" "github.com/go-acme/lego/v4/providers/dns/bluecat" + "github.com/go-acme/lego/v4/providers/dns/bluecatv2" "github.com/go-acme/lego/v4/providers/dns/bookmyname" "github.com/go-acme/lego/v4/providers/dns/brandit" "github.com/go-acme/lego/v4/providers/dns/bunny" @@ -34,16 +37,20 @@ import ( "github.com/go-acme/lego/v4/providers/dns/cloudns" "github.com/go-acme/lego/v4/providers/dns/cloudru" "github.com/go-acme/lego/v4/providers/dns/cloudxns" + "github.com/go-acme/lego/v4/providers/dns/com35" "github.com/go-acme/lego/v4/providers/dns/conoha" "github.com/go-acme/lego/v4/providers/dns/conohav3" "github.com/go-acme/lego/v4/providers/dns/constellix" "github.com/go-acme/lego/v4/providers/dns/corenetworks" "github.com/go-acme/lego/v4/providers/dns/cpanel" + "github.com/go-acme/lego/v4/providers/dns/czechia" + "github.com/go-acme/lego/v4/providers/dns/ddnss" "github.com/go-acme/lego/v4/providers/dns/derak" "github.com/go-acme/lego/v4/providers/dns/desec" "github.com/go-acme/lego/v4/providers/dns/designate" "github.com/go-acme/lego/v4/providers/dns/digitalocean" "github.com/go-acme/lego/v4/providers/dns/directadmin" + "github.com/go-acme/lego/v4/providers/dns/dnsexit" "github.com/go-acme/lego/v4/providers/dns/dnshomede" "github.com/go-acme/lego/v4/providers/dns/dnsimple" "github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy" @@ -61,6 +68,8 @@ import ( "github.com/go-acme/lego/v4/providers/dns/edgeone" "github.com/go-acme/lego/v4/providers/dns/efficientip" "github.com/go-acme/lego/v4/providers/dns/epik" + "github.com/go-acme/lego/v4/providers/dns/eurodns" + "github.com/go-acme/lego/v4/providers/dns/excedo" "github.com/go-acme/lego/v4/providers/dns/exec" "github.com/go-acme/lego/v4/providers/dns/exoscale" "github.com/go-acme/lego/v4/providers/dns/f5xc" @@ -94,9 +103,13 @@ import ( "github.com/go-acme/lego/v4/providers/dns/ionos" "github.com/go-acme/lego/v4/providers/dns/ionoscloud" "github.com/go-acme/lego/v4/providers/dns/ipv64" + "github.com/go-acme/lego/v4/providers/dns/ispconfig" + "github.com/go-acme/lego/v4/providers/dns/ispconfigddns" "github.com/go-acme/lego/v4/providers/dns/iwantmyname" + "github.com/go-acme/lego/v4/providers/dns/jdcloud" "github.com/go-acme/lego/v4/providers/dns/joker" "github.com/go-acme/lego/v4/providers/dns/keyhelp" + "github.com/go-acme/lego/v4/providers/dns/leaseweb" "github.com/go-acme/lego/v4/providers/dns/liara" "github.com/go-acme/lego/v4/providers/dns/lightsail" "github.com/go-acme/lego/v4/providers/dns/limacity" @@ -117,6 +130,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/namecheap" "github.com/go-acme/lego/v4/providers/dns/namedotcom" "github.com/go-acme/lego/v4/providers/dns/namesilo" + "github.com/go-acme/lego/v4/providers/dns/namesurfer" "github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech" "github.com/go-acme/lego/v4/providers/dns/neodigit" "github.com/go-acme/lego/v4/providers/dns/netcup" @@ -158,6 +172,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/technitium" "github.com/go-acme/lego/v4/providers/dns/tencentcloud" "github.com/go-acme/lego/v4/providers/dns/timewebcloud" + "github.com/go-acme/lego/v4/providers/dns/todaynic" "github.com/go-acme/lego/v4/providers/dns/transip" "github.com/go-acme/lego/v4/providers/dns/ultradns" "github.com/go-acme/lego/v4/providers/dns/uniteddomains" @@ -197,8 +212,12 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return aliesa.NewDNSProvider() case "allinkl": return allinkl.NewDNSProvider() + case "alwaysdata": + return alwaysdata.NewDNSProvider() case "anexia": return anexia.NewDNSProvider() + case "artfiles": + return artfiles.NewDNSProvider() case "arvancloud": return arvancloud.NewDNSProvider() case "auroradns": @@ -223,6 +242,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return bindman.NewDNSProvider() case "bluecat": return bluecat.NewDNSProvider() + case "bluecatv2": + return bluecatv2.NewDNSProvider() case "bookmyname": return bookmyname.NewDNSProvider() case "brandit": @@ -243,6 +264,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return cloudru.NewDNSProvider() case "cloudxns": return cloudxns.NewDNSProvider() + case "com35": + return com35.NewDNSProvider() case "conoha": return conoha.NewDNSProvider() case "conohav3": @@ -253,6 +276,10 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return corenetworks.NewDNSProvider() case "cpanel": return cpanel.NewDNSProvider() + case "czechia": + return czechia.NewDNSProvider() + case "ddnss": + return ddnss.NewDNSProvider() case "derak": return derak.NewDNSProvider() case "desec": @@ -263,6 +290,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return digitalocean.NewDNSProvider() case "directadmin": return directadmin.NewDNSProvider() + case "dnsexit": + return dnsexit.NewDNSProvider() case "dnshomede": return dnshomede.NewDNSProvider() case "dnsimple": @@ -297,6 +326,10 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return efficientip.NewDNSProvider() case "epik": return epik.NewDNSProvider() + case "eurodns": + return eurodns.NewDNSProvider() + case "excedo": + return excedo.NewDNSProvider() case "exec": return exec.NewDNSProvider() case "exoscale": @@ -363,12 +396,20 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return ionoscloud.NewDNSProvider() case "ipv64": return ipv64.NewDNSProvider() + case "ispconfig": + return ispconfig.NewDNSProvider() + case "ispconfigddns": + return ispconfigddns.NewDNSProvider() case "iwantmyname": return iwantmyname.NewDNSProvider() + case "jdcloud": + return jdcloud.NewDNSProvider() case "joker": return joker.NewDNSProvider() case "keyhelp": return keyhelp.NewDNSProvider() + case "leaseweb": + return leaseweb.NewDNSProvider() case "liara": return liara.NewDNSProvider() case "lightsail": @@ -409,6 +450,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return namedotcom.NewDNSProvider() case "namesilo": return namesilo.NewDNSProvider() + case "namesurfer": + return namesurfer.NewDNSProvider() case "nearlyfreespeech": return nearlyfreespeech.NewDNSProvider() case "neodigit": @@ -491,6 +534,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return tencentcloud.NewDNSProvider() case "timewebcloud": return timewebcloud.NewDNSProvider() + case "todaynic": + return todaynic.NewDNSProvider() case "transip": return transip.NewDNSProvider() case "ultradns":