Compare commits

..

34 commits

Author SHA1 Message Date
Ludovic Fernandez
87b172f103
gigahostno: remove unused Zone fields (#2913) 2026-03-12 21:27:46 +01:00
exsesa
9be8cd43ae
Add DNS provider for Excedo (#2910)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2026-03-10 22:58:13 +01:00
Dane
7b1aa50081
safedns: rename UKFast SafeDNS to ANS SafeDNS (#2877)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2026-03-08 23:02:14 +00:00
Ludovic Fernandez
a56697ed1c
Add DNS provider for EuroDNS (#2898) 2026-03-08 23:32:56 +01:00
Ludovic Fernandez
847c763504
feat: Add DNS provider for Czechia (#2885) 2026-03-04 18:04:04 +01:00
Fernandez Ludovic
da51631cd3
chore: improve issue template 2026-03-04 17:59:58 +01:00
Ludovic Fernandez
491dcaad1d
feat: allow to Unwrap obtainError (#2874) 2026-02-25 23:12:43 +01:00
Ludovic Fernandez
7d459b59c5
liara: add support for team ID (#2867) 2026-02-22 14:14:18 +01:00
Fernandez Ludovic
4547c4317e
Detach v4.32.0 2026-02-19 13:14:28 +01:00
Fernandez Ludovic
c50918c54e
Prepare release v4.32.0 2026-02-19 13:14:13 +01:00
Ludovic Fernandez
dd1ea80c08
Add DNS provider for Leaseweb (#2856) 2026-02-19 12:52:04 +01:00
Ludovic Fernandez
078a1889c8
Add DNS provider for ArtFiles (#2859) 2026-02-19 12:26:53 +01:00
Ludovic Fernandez
d896c1f036
fix: preserve domain order (#2862) 2026-02-19 12:25:10 +01:00
Ludovic Fernandez
84f3be40f0
chore: update dependencies (#2860) 2026-02-18 23:26:31 +01:00
Ludovic Fernandez
94e3bfb96a
chore: update linter (#2857) 2026-02-17 23:34:27 +01:00
Ludovic Fernandez
c06f378f0e
namesurfer: fix updateDNSHost (#2854) 2026-02-15 10:42:48 +01:00
Grigas Šukys
2e095b95a5
Add DNS provider for FusionLayer NameSurfer (#2852)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2026-02-15 02:22:35 +00:00
Ludovic Fernandez
4a61728ff0
fix: deduplicate authz for DNS01 challenge (#2828) 2026-02-12 19:13:49 +01:00
Ludovic Fernandez
1991339cc1
timewebcloud: fix subdomain support (#2845) 2026-02-12 18:20:05 +01:00
Ludovic Fernandez
dc51d5ee65
chore: use memcache Docker image directly (#2846) 2026-02-12 18:01:57 +01:00
Fernandez Ludovic
4c6d29882e
chore: update linter, and workflows 2026-02-12 17:32:23 +01:00
Andy Warner
c1aaf19aac
docs: make it more clear that any ACME CA may be used (#2841) 2026-02-10 02:51:54 +01:00
Mortie Torabi
fac5c39f5f
fix: implement parsing for Retry-After header according to RFC 7231 (#2830)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2026-01-30 20:36:46 +01:00
Ludovic Fernandez
a7145a29ac
fix: use IPs to define the main domain (#2817) 2026-01-28 18:41:23 +01:00
Ludovic Fernandez
2ce04a6586
alidns: add line record option (#2814) 2026-01-25 14:51:12 +01:00
Fernandez Ludovic
44b89b7e92
allinkl: factorize findZone 2026-01-22 05:10:35 +01:00
Ludovic Fernandez
16894fb99e
allinkl: detect zone through API (#2721) 2026-01-20 17:59:42 +01:00
Ludovic Fernandez
de869c8a7e
Add DNS provider for Bluecat v2 (#2791) 2026-01-19 17:31:56 +01:00
Ameer Ghani
05333f3c84
chore: improve warning message about backups (#2797) 2026-01-16 20:13:42 +00:00
Ludovic Fernandez
4d41c52db8
Add DNS provider for DDNSS (#2795) 2026-01-15 23:16:02 +01:00
Ludovic Fernandez
d063b15c02
azure: reinforces deprecation (#2792) 2026-01-15 01:04:30 +01:00
Ludovic Fernandez
527d51d485
Add DNS provider for DNSExit (#2787) 2026-01-12 17:04:28 +00:00
Ludovic Fernandez
7f10c131f4
Add DNS provider for TodayNIC/时代互联 (#2788) 2026-01-12 17:50:21 +01:00
Fernandez Ludovic
ac1092710d Detach v4.31.0 2026-01-08 17:58:41 +01:00
202 changed files with 11883 additions and 478 deletions

View file

@ -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

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
env:
GO_VERSION: stable
GOLANGCI_LINT_VERSION: v2.8.0
GOLANGCI_LINT_VERSION: v2.10
HUGO_VERSION: 0.148.2
CGO_ENABLED: 0
LEGO_E2E_TESTS: CI
@ -50,7 +50,7 @@ jobs:
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: |

View file

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

View file

@ -6,6 +6,36 @@ Everybody thinks that the others will donate, but in the end, nobody does.
So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev).
## v4.32.0
- Release date: 2026-02-19
- Tag: [v4.32.0](https://github.com/go-acme/lego/releases/tag/v4.32.0)
### Added
- **[dnsprovider]** Add DNS provider for ArtFiles
- **[dnsprovider]** Add DNS provider for Leaseweb
- **[dnsprovider]** Add DNS provider for FusionLayer NameSurfer
- **[dnsprovider]** Add DNS provider for DDNSS
- **[dnsprovider]** Add DNS provider for Bluecat v2
- **[dnsprovider]** Add DNS provider for TodayNIC/时代互联
- **[dnsprovider]** Add DNS provider for DNSExit
- **[dnsprovider]** alidns: add line record option
### Changed
- **[dnsprovider]** azure: reinforces deprecation
- **[dnsprovider]** allinkl: detect zone through API
### Fixed
- **[ari]** fix: implement parsing for Retry-After header according to RFC 7231
- **[dnsprovider]** namesurfer: fix updateDNSHost
- **[dnsprovider]** timewebcloud: fix subdomain support
- **[dnsprovider]** fix: deduplicate authz for DNS01 challenge
- **[lib,cli]** fix: use IPs to define the main domain
- **[lib]** fix: preserve domain order
## v4.31.0
- Release date: 2026-01-08

102
README.md
View file

@ -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)
@ -73,223 +73,233 @@ If your DNS provider is not supported, please open an [issue](https://github.com
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/route53/">Amazon Route 53</a></td>
<td><a href="https://go-acme.github.io/lego/dns/anexia/">Anexia CloudDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/safedns/">ANS SafeDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/artfiles/">ArtFiles</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/arvancloud/">ArvanCloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/auroradns/">Aurora DNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/autodns/">Autodns</a></td>
<td><a href="https://go-acme.github.io/lego/dns/axelname/">Axelname</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/azion/">Azion</a></td>
<td><a href="https://go-acme.github.io/lego/dns/azure/">Azure (deprecated)</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/azuredns/">Azure DNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/baiducloud/">Baidu Cloud</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/beget/">Beget.com</a></td>
<td><a href="https://go-acme.github.io/lego/dns/binarylane/">Binary Lane</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/bindman/">Bindman</a></td>
<td><a href="https://go-acme.github.io/lego/dns/bluecat/">Bluecat</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/bluecatv2/">Bluecat v2</a></td>
<td><a href="https://go-acme.github.io/lego/dns/bookmyname/">BookMyName</a></td>
<td><a href="https://go-acme.github.io/lego/dns/brandit/">Brandit (deprecated)</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/bunny/">Bunny</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/checkdomain/">Checkdomain</a></td>
<td><a href="https://go-acme.github.io/lego/dns/civo/">Civo</a></td>
<td><a href="https://go-acme.github.io/lego/dns/cloudru/">Cloud.ru</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/clouddns/">CloudDNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/cloudflare/">Cloudflare</a></td>
<td><a href="https://go-acme.github.io/lego/dns/cloudns/">ClouDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/cloudxns/">CloudXNS (Deprecated)</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/conoha/">ConoHa v2</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/conohav3/">ConoHa v3</a></td>
<td><a href="https://go-acme.github.io/lego/dns/constellix/">Constellix</a></td>
<td><a href="https://go-acme.github.io/lego/dns/corenetworks/">Core-Networks</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/cpanel/">CPanel/WHM</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/czechia/">Czechia</a></td>
<td><a href="https://go-acme.github.io/lego/dns/ddnss/">DDnss (DynDNS Service)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/derak/">Derak Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/desec/">deSEC.io</a></td>
<td><a href="https://go-acme.github.io/lego/dns/designate/">Designate DNSaaS for Openstack</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/designate/">Designate DNSaaS for Openstack</a></td>
<td><a href="https://go-acme.github.io/lego/dns/digitalocean/">Digital Ocean</a></td>
<td><a href="https://go-acme.github.io/lego/dns/directadmin/">DirectAdmin</a></td>
<td><a href="https://go-acme.github.io/lego/dns/dnsmadeeasy/">DNS Made Easy</a></td>
<td><a href="https://go-acme.github.io/lego/dns/dnshomede/">dnsHome.de</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dnsexit/">DNSExit</a></td>
<td><a href="https://go-acme.github.io/lego/dns/dnshomede/">dnsHome.de</a></td>
<td><a href="https://go-acme.github.io/lego/dns/dnsimple/">DNSimple</a></td>
<td><a href="https://go-acme.github.io/lego/dns/dnspod/">DNSPod (deprecated)</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dode/">Domain Offensive (do.de)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/domeneshop/">Domeneshop</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dreamhost/">DreamHost</a></td>
<td><a href="https://go-acme.github.io/lego/dns/duckdns/">Duck DNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dyn/">Dyn</a></td>
<td><a href="https://go-acme.github.io/lego/dns/dyndnsfree/">DynDnsFree.de</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dynu/">Dynu</a></td>
<td><a href="https://go-acme.github.io/lego/dns/easydns/">EasyDNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/edgecenter/">EdgeCenter</a></td>
<td><a href="https://go-acme.github.io/lego/dns/efficientip/">Efficient IP</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/epik/">Epik</a></td>
<td><a href="https://go-acme.github.io/lego/dns/eurodns/">EuroDNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/excedo/">Excedo</a></td>
<td><a href="https://go-acme.github.io/lego/dns/exoscale/">Exoscale</a></td>
<td><a href="https://go-acme.github.io/lego/dns/exec/">External program</a></td>
<td><a href="https://go-acme.github.io/lego/dns/f5xc/">F5 XC</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/freemyip/">freemyip.com</a></td>
<td><a href="https://go-acme.github.io/lego/dns/namesurfer/">FusionLayer NameSurfer</a></td>
<td><a href="https://go-acme.github.io/lego/dns/gcore/">G-Core</a></td>
<td><a href="https://go-acme.github.io/lego/dns/gandi/">Gandi</a></td>
<td><a href="https://go-acme.github.io/lego/dns/gandiv5/">Gandi Live DNS (v5)</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/gandiv5/">Gandi Live DNS (v5)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/gigahostno/">Gigahost.no</a></td>
<td><a href="https://go-acme.github.io/lego/dns/glesys/">Glesys</a></td>
<td><a href="https://go-acme.github.io/lego/dns/godaddy/">Go Daddy</a></td>
<td><a href="https://go-acme.github.io/lego/dns/gcloud/">Google Cloud</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/gcloud/">Google Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/googledomains/">Google Domains</a></td>
<td><a href="https://go-acme.github.io/lego/dns/gravity/">Gravity</a></td>
<td><a href="https://go-acme.github.io/lego/dns/hetzner/">Hetzner</a></td>
<td><a href="https://go-acme.github.io/lego/dns/hostingde/">Hosting.de</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/hostingde/">Hosting.de</a></td>
<td><a href="https://go-acme.github.io/lego/dns/hostingnl/">Hosting.nl</a></td>
<td><a href="https://go-acme.github.io/lego/dns/hostinger/">Hostinger</a></td>
<td><a href="https://go-acme.github.io/lego/dns/hosttech/">Hosttech</a></td>
<td><a href="https://go-acme.github.io/lego/dns/httpreq/">HTTP request</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/httpreq/">HTTP request</a></td>
<td><a href="https://go-acme.github.io/lego/dns/httpnet/">http.net</a></td>
<td><a href="https://go-acme.github.io/lego/dns/huaweicloud/">Huawei Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/hurricane/">Hurricane Electric DNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/hyperone/">HyperOne</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/hyperone/">HyperOne</a></td>
<td><a href="https://go-acme.github.io/lego/dns/ibmcloud/">IBM Cloud (SoftLayer)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/iijdpf/">IIJ DNS Platform Service</a></td>
<td><a href="https://go-acme.github.io/lego/dns/infoblox/">Infoblox</a></td>
<td><a href="https://go-acme.github.io/lego/dns/infomaniak/">Infomaniak</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/infomaniak/">Infomaniak</a></td>
<td><a href="https://go-acme.github.io/lego/dns/iij/">Internet Initiative Japan</a></td>
<td><a href="https://go-acme.github.io/lego/dns/internetbs/">Internet.bs</a></td>
<td><a href="https://go-acme.github.io/lego/dns/inwx/">INWX</a></td>
<td><a href="https://go-acme.github.io/lego/dns/ionos/">Ionos</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/ionos/">Ionos</a></td>
<td><a href="https://go-acme.github.io/lego/dns/ionoscloud/">Ionos Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/ipv64/">IPv64</a></td>
<td><a href="https://go-acme.github.io/lego/dns/ispconfig/">ISPConfig 3</a></td>
<td><a href="https://go-acme.github.io/lego/dns/ispconfigddns/">ISPConfig 3 - Dynamic DNS (DDNS) Module</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/ispconfigddns/">ISPConfig 3 - Dynamic DNS (DDNS) Module</a></td>
<td><a href="https://go-acme.github.io/lego/dns/iwantmyname/">iwantmyname (Deprecated)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/jdcloud/">JD Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/joker/">Joker</a></td>
<td><a href="https://go-acme.github.io/lego/dns/acme-dns/">Joohoi&#39;s ACME-DNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/acme-dns/">Joohoi&#39;s ACME-DNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/keyhelp/">KeyHelp</a></td>
<td><a href="https://go-acme.github.io/lego/dns/leaseweb/">Leaseweb</a></td>
<td><a href="https://go-acme.github.io/lego/dns/liara/">Liara</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/limacity/">Lima-City</a></td>
<td><a href="https://go-acme.github.io/lego/dns/linode/">Linode (v4)</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/liquidweb/">Liquid Web</a></td>
<td><a href="https://go-acme.github.io/lego/dns/loopia/">Loopia</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/luadns/">LuaDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/mailinabox/">Mail-in-a-Box</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/manageengine/">ManageEngine CloudDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/manual/">Manual</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/metaname/">Metaname</a></td>
<td><a href="https://go-acme.github.io/lego/dns/metaregistrar/">Metaregistrar</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/mijnhost/">mijn.host</a></td>
<td><a href="https://go-acme.github.io/lego/dns/mittwald/">Mittwald</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/myaddr/">myaddr.{tools,dev,io}</a></td>
<td><a href="https://go-acme.github.io/lego/dns/mydnsjp/">MyDNS.jp</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/mythicbeasts/">MythicBeasts</a></td>
<td><a href="https://go-acme.github.io/lego/dns/namedotcom/">Name.com</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/namecheap/">Namecheap</a></td>
<td><a href="https://go-acme.github.io/lego/dns/namesilo/">Namesilo</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/nearlyfreespeech/">NearlyFreeSpeech.NET</a></td>
<td><a href="https://go-acme.github.io/lego/dns/neodigit/">Neodigit</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/netcup/">Netcup</a></td>
<td><a href="https://go-acme.github.io/lego/dns/netlify/">Netlify</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/nicmanager/">Nicmanager</a></td>
<td><a href="https://go-acme.github.io/lego/dns/nifcloud/">NIFCloud</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/njalla/">Njalla</a></td>
<td><a href="https://go-acme.github.io/lego/dns/nodion/">Nodion</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/ns1/">NS1</a></td>
<td><a href="https://go-acme.github.io/lego/dns/octenium/">Octenium</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/otc/">Open Telekom Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/oraclecloud/">Oracle Cloud</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/ovh/">OVH</a></td>
<td><a href="https://go-acme.github.io/lego/dns/plesk/">plesk.com</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/porkbun/">Porkbun</a></td>
<td><a href="https://go-acme.github.io/lego/dns/pdns/">PowerDNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/rackspace/">Rackspace</a></td>
<td><a href="https://go-acme.github.io/lego/dns/rainyun/">Rain Yun/雨云</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/rcodezero/">RcodeZero</a></td>
<td><a href="https://go-acme.github.io/lego/dns/regru/">reg.ru</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/regfish/">Regfish</a></td>
<td><a href="https://go-acme.github.io/lego/dns/rfc2136/">RFC2136</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/rimuhosting/">RimuHosting</a></td>
<td><a href="https://go-acme.github.io/lego/dns/nicru/">RU CENTER</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/sakuracloud/">Sakura Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/scaleway/">Scaleway</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/selectel/">Selectel</a></td>
<td><a href="https://go-acme.github.io/lego/dns/selectelv2/">Selectel v2</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/selfhostde/">SelfHost.(de|eu)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/servercow/">Servercow</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/shellrent/">Shellrent</a></td>
<td><a href="https://go-acme.github.io/lego/dns/simply/">Simply.com</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/sonic/">Sonic</a></td>
<td><a href="https://go-acme.github.io/lego/dns/spaceship/">Spaceship</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/stackpath/">Stackpath</a></td>
<td><a href="https://go-acme.github.io/lego/dns/syse/">Syse</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/technitium/">Technitium</a></td>
<td><a href="https://go-acme.github.io/lego/dns/tencentcloud/">Tencent Cloud DNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/edgeone/">Tencent EdgeOne</a></td>
<td><a href="https://go-acme.github.io/lego/dns/timewebcloud/">Timeweb Cloud</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/todaynic/">TodayNIC/时代互联</a></td>
<td><a href="https://go-acme.github.io/lego/dns/transip/">TransIP</a></td>
<td><a href="https://go-acme.github.io/lego/dns/safedns/">UKFast SafeDNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/ultradns/">Ultradns</a></td>
<td><a href="https://go-acme.github.io/lego/dns/uniteddomains/">United-Domains</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/variomedia/">Variomedia</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vegadns/">VegaDNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/vercel/">Vercel</a></td>
<td><a href="https://go-acme.github.io/lego/dns/versio/">Versio.[nl|eu|uk]</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/vinyldns/">VinylDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/virtualname/">Virtualname</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/vkcloud/">VK Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/volcengine/">Volcano Engine/火山引擎</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/vscale/">Vscale</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vultr/">Vultr</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/webnamesca/">webnames.ca</a></td>
<td><a href="https://go-acme.github.io/lego/dns/webnames/">webnames.ru</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/websupport/">Websupport</a></td>
<td><a href="https://go-acme.github.io/lego/dns/wedos/">WEDOS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/westcn/">West.cn/西部数码</a></td>
<td><a href="https://go-acme.github.io/lego/dns/yandex360/">Yandex 360</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/yandexcloud/">Yandex Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/yandex/">Yandex PDD</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/zoneee/">Zone.ee</a></td>
<td><a href="https://go-acme.github.io/lego/dns/zoneedit/">ZoneEdit</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/zonomi/">Zonomi</a></td>
<td></td>
<td></td>
<td></td>
</tr></table>
<!-- END DNS PROVIDERS LIST -->

View file

@ -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].

View file

@ -4,10 +4,10 @@ package sender
const (
// ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "xenolf-acme/4.31.0"
ourUserAgent = "xenolf-acme/4.32.0"
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
// values: detach|release
// NOTE: Update this with each tagged release.
ourUserAgentComment = "release"
ourUserAgentComment = "detach"
)

View file

@ -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)
}

View file

@ -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)
})
}
}

View file

@ -29,18 +29,18 @@ type ProblemDetails struct {
}
func (p *ProblemDetails) Error() string {
var msg strings.Builder
msg := new(strings.Builder)
msg.WriteString(fmt.Sprintf("acme: error: %d", p.HTTPStatus))
_, _ = fmt.Fprintf(msg, "acme: error: %d", p.HTTPStatus)
if p.Method != "" || p.URL != "" {
msg.WriteString(fmt.Sprintf(" :: %s :: %s", p.Method, p.URL))
_, _ = fmt.Fprintf(msg, " :: %s :: %s", p.Method, p.URL)
}
msg.WriteString(fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail))
_, _ = fmt.Fprintf(msg, " :: %s :: %s", p.Type, p.Detail)
for _, sub := range p.SubProblems {
msg.WriteString(fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail))
_, _ = fmt.Fprintf(msg, ", problem: %q :: %s", sub.Type, sub.Detail)
}
if p.Instance != "" {

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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))
}

View file

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

View file

@ -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)
}
}()

View file

@ -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,
}
}

View file

@ -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))
}
})
}
}

View file

@ -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) {

View file

@ -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, ", ")
}

View file

@ -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 {

View file

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

View file

@ -19,6 +19,7 @@ func allDNSCodes() string {
"allinkl",
"alwaysdata",
"anexia",
"artfiles",
"arvancloud",
"auroradns",
"autodns",
@ -31,6 +32,7 @@ func allDNSCodes() string {
"binarylane",
"bindman",
"bluecat",
"bluecatv2",
"bookmyname",
"brandit",
"bunny",
@ -47,11 +49,14 @@ func allDNSCodes() string {
"constellix",
"corenetworks",
"cpanel",
"czechia",
"ddnss",
"derak",
"desec",
"designate",
"digitalocean",
"directadmin",
"dnsexit",
"dnshomede",
"dnsimple",
"dnsmadeeasy",
@ -69,6 +74,8 @@ func allDNSCodes() string {
"edgeone",
"efficientip",
"epik",
"eurodns",
"excedo",
"exec",
"exoscale",
"f5xc",
@ -108,6 +115,7 @@ func allDNSCodes() string {
"jdcloud",
"joker",
"keyhelp",
"leaseweb",
"liara",
"lightsail",
"limacity",
@ -128,6 +136,7 @@ func allDNSCodes() string {
"namecheap",
"namedotcom",
"namesilo",
"namesurfer",
"nearlyfreespeech",
"neodigit",
"netcup",
@ -169,6 +178,7 @@ func allDNSCodes() string {
"technitium",
"tencentcloud",
"timewebcloud",
"todaynic",
"transip",
"ultradns",
"uniteddomains",
@ -259,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()
@ -351,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.`)
@ -620,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.`)
@ -971,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.`)
@ -1086,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.`)
@ -1445,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.`)
@ -2263,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.`)
@ -2278,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()
@ -2670,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.`)
@ -3233,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()
@ -3574,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.`)

View file

@ -58,8 +58,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `ALICLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |
| `ALICLOUD_LINE` | Line (Default: default) |
| `ALICLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
| `ALICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
| `ALICLOUD_REGION_ID` | Region ID (Default: cn-hangzhou) |
| `ALICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.

69
docs/content/dns/zz_gen_artfiles.md generated Normal file
View file

@ -0,0 +1,69 @@
---
title: "ArtFiles"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: artfiles
dnsprovider:
since: "v4.32.0"
code: "artfiles"
url: "https://www.artfiles.de/extras/domains/"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/artfiles/artfiles.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [ArtFiles](https://www.artfiles.de/extras/domains/).
<!--more-->
- Code: `artfiles`
- Since: v4.32.0
Here is an example bash command using the ArtFiles provider:
```bash
ARTFILES_USERNAME="xxx" \
ARTFILES_PASSWORD="yyy" \
lego --dns artfiles -d '*.example.com' -d example.com run
```
## Credentials
| Environment Variable Name | Description |
|-----------------------|-------------|
| `ARTFILES_PASSWORD` | API password |
| `ARTFILES_USERNAME` | API username |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## Additional Configuration
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `ARTFILES_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
| `ARTFILES_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
| `ARTFILES_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 360) |
| `ARTFILES_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## More information
- [API documentation](https://support.artfiles.de/DCP-API#dns)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/artfiles/artfiles.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

76
docs/content/dns/zz_gen_bluecatv2.md generated Normal file
View file

@ -0,0 +1,76 @@
---
title: "Bluecat v2"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: bluecatv2
dnsprovider:
since: "v4.32.0"
code: "bluecatv2"
url: "https://www.bluecatnetworks.com"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/bluecatv2/bluecatv2.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [Bluecat v2](https://www.bluecatnetworks.com).
<!--more-->
- Code: `bluecatv2`
- Since: v4.32.0
Here is an example bash command using the Bluecat v2 provider:
```bash
BLUECATV2_SERVER_URL="https://example.com" \
BLUECATV2_USERNAME="xxx" \
BLUECATV2_PASSWORD="yyy" \
BLUECATV2_CONFIG_NAME="myConfiguration" \
BLUECATV2_VIEW_NAME="myView" \
lego --dns bluecatv2 -d '*.example.com' -d example.com run
```
## Credentials
| Environment Variable Name | Description |
|-----------------------|-------------|
| `BLUECATV2_CONFIG_NAME` | Configuration name |
| `BLUECATV2_PASSWORD` | API password |
| `BLUECATV2_USERNAME` | API username |
| `BLUECATV2_VIEW_NAME` | DNS View Name |
| `BLUECAT_SERVER_URL` | The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## Additional Configuration
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `BLUECATV2_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
| `BLUECATV2_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
| `BLUECATV2_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
| `BLUECATV2_SKIP_DEPLOY` | Skip quick deployements |
| `BLUECATV2_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## More information
- [API documentation](https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Introduction/9.6.0)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/bluecatv2/bluecatv2.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

67
docs/content/dns/zz_gen_czechia.md generated Normal file
View file

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

68
docs/content/dns/zz_gen_ddnss.md generated Normal file
View file

@ -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/"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/ddnss/ddnss.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [DDnss (DynDNS Service)](https://ddnss.de/).
<!--more-->
- 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)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/ddnss/ddnss.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

67
docs/content/dns/zz_gen_dnsexit.md generated Normal file
View file

@ -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"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/dnsexit/dnsexit.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [DNSExit](https://dnsexit.com).
<!--more-->
- 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/)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/dnsexit/dnsexit.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

69
docs/content/dns/zz_gen_eurodns.md generated Normal file
View file

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

69
docs/content/dns/zz_gen_excedo.md generated Normal file
View file

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

67
docs/content/dns/zz_gen_leaseweb.md generated Normal file
View file

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

View file

@ -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.

View file

@ -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

73
docs/content/dns/zz_gen_namesurfer.md generated Normal file
View file

@ -0,0 +1,73 @@
---
title: "FusionLayer NameSurfer"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: namesurfer
dnsprovider:
since: "v4.32.0"
code: "namesurfer"
url: "https://www.fusionlayer.com/"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/namesurfer/namesurfer.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [FusionLayer NameSurfer](https://www.fusionlayer.com/).
<!--more-->
- Code: `namesurfer`
- Since: v4.32.0
Here is an example bash command using the FusionLayer NameSurfer provider:
```bash
NAMESURFER_BASE_URL=https://foo.example.com:8443/API/NSService_10 \
NAMESURFER_API_KEY=xxx \
NAMESURFER_API_SECRET=yyy \
lego --dns namesurfer -d '*.example.com' -d example.com run
```
## Credentials
| Environment Variable Name | Description |
|-----------------------|-------------|
| `NAMESURFER_API_KEY` | API key name |
| `NAMESURFER_API_SECRET` | API secret |
| `NAMESURFER_BASE_URL` | The base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## Additional Configuration
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `NAMESURFER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
| `NAMESURFER_INSECURE_SKIP_VERIFY` | Whether to verify the API certificate |
| `NAMESURFER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
| `NAMESURFER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |
| `NAMESURFER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |
| `NAMESURFER_VIEW` | DNS view name (optional, default: empty string) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## More information
- [API documentation](https://web.archive.org/web/20260213170737/http://95.128.3.201:8053/API/NSService_10)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/namesurfer/namesurfer.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

View file

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

69
docs/content/dns/zz_gen_todaynic.md generated Normal file
View file

@ -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/"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/todaynic/todaynic.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [TodayNIC/时代互联](https://www.todaynic.com/).
<!--more-->
- 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)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/todaynic/todaynic.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

View file

@ -152,7 +152,7 @@ To display the documentation for a specific DNS provider, run:
$ lego dnshelp -c code
Supported DNS providers:
acme-dns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, 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, ispconfig, ispconfigddns, iwantmyname, jdcloud, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, neodigit, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, 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
"""

109
go.mod
View file

@ -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
@ -15,28 +15,28 @@ require (
github.com/Azure/go-autorest/autorest/to v0.4.1
github.com/BurntSushi/toml v1.6.0
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15
github.com/alibabacloud-go/tea v1.4.0
github.com/aliyun/credentials-go v1.4.7
github.com/aws/aws-sdk-go-v2 v1.41.0
github.com/aws/aws-sdk-go-v2/config v1.32.6
github.com/aws/aws-sdk-go-v2/credentials v1.19.6
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5
github.com/aws/aws-sdk-go-v2 v1.41.1
github.com/aws/aws-sdk-go-v2/config v1.32.8
github.com/aws/aws-sdk-go-v2/credentials v1.19.8
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6
github.com/aziontech/azionapi-go-sdk v0.144.0
github.com/baidubce/bce-sdk-go v0.9.256
github.com/baidubce/bce-sdk-go v0.9.260
github.com/cenkalti/backoff/v5 v5.0.3
github.com/dnsimple/dnsimple-go/v4 v4.0.0
github.com/exoscale/egoscale/v3 v3.1.33
github.com/go-acme/alidns-20150109/v4 v4.7.0
github.com/go-acme/esa-20240910/v2 v2.44.0
github.com/go-acme/esa-20240910/v2 v2.48.0
github.com/go-acme/jdcloud-sdk-go v1.64.0
github.com/go-acme/tencentclouddnspod v1.1.25
github.com/go-acme/tencentedgdeone v1.1.48
github.com/go-acme/tencentclouddnspod v1.3.24
github.com/go-acme/tencentedgdeone v1.3.38
github.com/go-jose/go-jose/v4 v4.1.3
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/google/go-cmp v0.7.0
github.com/google/go-querystring v1.2.0
github.com/google/uuid v1.6.0
@ -44,18 +44,18 @@ require (
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/hashicorp/go-version v1.8.0
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0
github.com/labbsr0x/bindman-dns-webhook v1.0.2
github.com/ldez/grignotin v0.10.1
github.com/linode/linodego v1.64.0
github.com/linode/linodego v1.65.0
github.com/liquidweb/liquidweb-go v1.6.4
github.com/mattn/go-isatty v0.0.20
github.com/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
@ -65,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.2
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2
github.com/nrdcg/porkbun v0.4.0
github.com/nrdcg/vegadns v0.3.0
github.com/nzdjb/go-metaname v1.0.0
@ -81,29 +81,29 @@ require (
github.com/selectel/go-selvpcclient/v4 v4.1.0
github.com/softlayer/softlayer-go v1.2.1
github.com/stretchr/testify v1.11.1
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48
github.com/transip/gotransip/v6 v6.26.1
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419
github.com/urfave/cli/v2 v2.27.7
github.com/vinyldns/go-vinyldns v0.9.17
github.com/volcengine/volc-sdk-golang v1.0.233
github.com/vultr/govultr/v3 v3.26.1
github.com/yandex-cloud/go-genproto v0.43.0
github.com/yandex-cloud/go-sdk/services/dns v0.0.25
github.com/yandex-cloud/go-sdk/v2 v2.37.0
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.259.0
gopkg.in/ns1/ns1-go.v2 v2.16.0
google.golang.org/api v0.267.0
gopkg.in/ns1/ns1-go.v2 v2.17.2
gopkg.in/yaml.v2 v2.4.0
software.sslmate.com/src/go-pkcs12 v0.7.0
)
require (
cloud.google.com/go/auth v0.18.0 // indirect
cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
@ -119,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.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@ -160,8 +161,8 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
@ -205,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-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ini.v1 v1.67.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

233
go.sum
View file

@ -13,8 +13,8 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
@ -42,8 +42,8 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
@ -121,8 +121,10 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 h1:Mubp9hXZMTPWZK+WxrR+kKOVFp4Q/PDZrIIM7ByXI9Y=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
@ -169,54 +171,54 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/config v1.32.8 h1:iu+64gwDKEoKnyTQskSku72dAwggKI5sV6rNvgSMpMs=
github.com/aws/aws-sdk-go-v2/config v1.32.8/go.mod h1:MI2XvA+qDi3i9AJxX1E2fu730syEBzp/jnXrjxuHwgI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.8 h1:Jp2JYH1lRT3KhX4mshHPvVYsR5qqRec3hGvEarNYoR0=
github.com/aws/aws-sdk-go-v2/credentials v1.19.8/go.mod h1:fZG9tuvyVfxknv1rKibIz3DobRaFw1Poe8IKtXB3XYY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10 h1:MQuZZ6Tq1qQabPlkVxrCMdyVl70Ogl4AERZKo+y9Wzo=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10/go.mod h1:U5C3JME1ibKESmpzBAqlRpTYZfVbTqrb5ICJm+sVVd8=
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 h1:80pDB3Tpmb2RCSZORrK9/3iQxsd+w6vSzVqpT1FGiwE=
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0/go.mod h1:6EZUGGNLPLh5Unt30uEoA+KQcByERfXIkax9qrc80nA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 h1:VM5e5M39zRSs+aT0O9SoxHjUXqXxhbw3Yi0FdMQWPIc=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11/go.mod h1:0jvzYPIQGCpnY/dmdaotTk2JH4QuBlnW0oeyrcGLWJ4=
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0DINACb1wxM6wdGlx4eHE=
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aziontech/azionapi-go-sdk v0.144.0 h1:T+/w18o+FCiZsk3Z0ACBVVe7c/5EGLG15S3P8JfuPfo=
github.com/aziontech/azionapi-go-sdk v0.144.0/go.mod h1:OKxP/R0iVXnJJakYwMhh2BGAXnud8Ruy55Ak9ANuWoU=
github.com/baidubce/bce-sdk-go v0.9.256 h1:/6UwBzDp+dRFpKRIb5WsvxfSiG4SLOIOghvagOK/q4Y=
github.com/baidubce/bce-sdk-go v0.9.256/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/baidubce/bce-sdk-go v0.9.260 h1:1v1+2GTP+NGK3L24rJ+bnoiTaDaIy2YoaUM+ot2GTcw=
github.com/baidubce/bce-sdk-go v0.9.260/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@ -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=
@ -315,14 +319,14 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-acme/alidns-20150109/v4 v4.7.0 h1:PqJ/wR0JTpL4v0Owu1uM7bPQ1Yww0eQLAuuSdLjjQaQ=
github.com/go-acme/alidns-20150109/v4 v4.7.0/go.mod h1:btQvB6xZoN6ykKB74cPhiR+uvhrEE2AFVXm6RDmCHm0=
github.com/go-acme/esa-20240910/v2 v2.44.0 h1:ACi2uFb7ig4ousFs/YiFBR+aw3A4SHtOxvkMWB2Hbcs=
github.com/go-acme/esa-20240910/v2 v2.44.0/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g=
github.com/go-acme/esa-20240910/v2 v2.48.0 h1:muSDyhjDTejxUGe3FTthCPCqRaEdYY9cG3N/AmU52Lc=
github.com/go-acme/esa-20240910/v2 v2.48.0/go.mod h1:shPb6hzc1rJL15IJBY8HQ4GZk4E8RC52+52twutEwIg=
github.com/go-acme/jdcloud-sdk-go v1.64.0 h1:AW9j5khk8tRYbpBJPxKmqdwIqgLs2Fz3HUK3hn2YXjs=
github.com/go-acme/jdcloud-sdk-go v1.64.0/go.mod h1:qc/m8HNX1Zgd7GAv2DSEinup8fwy3Ted3/VVx7LB5bU=
github.com/go-acme/tencentclouddnspod v1.1.25 h1:7H3ZKshkaHzCXfRpAHVB5nvxeDDl2XLeNZfrNHiZj/s=
github.com/go-acme/tencentclouddnspod v1.1.25/go.mod h1:XXfzp0AYV7UAUsHKT6R0KAUJFhqAUXmWGF07Elpa5cE=
github.com/go-acme/tencentedgdeone v1.1.48 h1:WLyLBsRVhSLFmtbEFXk0naLODSQn7X6J0Fc/qR8xVUk=
github.com/go-acme/tencentedgdeone v1.1.48/go.mod h1:mu6tA+bPhlSd+CKUfzRikE0mfxmTlBI6dVTn9LY9dRI=
github.com/go-acme/tencentclouddnspod v1.3.24 h1:uCSiOW1EJttcnOON+MVVyVDJguFL/Q4NIGkq1CrT9p8=
github.com/go-acme/tencentclouddnspod v1.3.24/go.mod h1:RKcB2wSoZncjBA0OEFj59s1ko1XDy+ZsAtk+9uMxUF0=
github.com/go-acme/tencentedgdeone v1.3.38 h1:5YsVl0H4A+cwtiUqR1eZbKFdr4OWfYp2KYJopifzKyQ=
github.com/go-acme/tencentedgdeone v1.3.38/go.mod h1:yyjTKVmGpMtFv5HqGODqehHnZJ4KWAbG6dAiwWDgCDY=
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@ -366,8 +370,8 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8Wd
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
@ -467,12 +471,12 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw=
github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
@ -537,8 +541,8 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182 h1:B3W9acgpqu5XsN8v+W8SOTfqn/6n4JsjgoKBsm30HFY=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 h1:J+U6+eUjIsBhefolFdZW5hQNJbkMj+7msxZrv56Cg2g=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -612,8 +616,8 @@ github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufp
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY=
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM=
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
@ -649,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=
@ -691,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=
@ -711,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.2 h1:l0tH15ACQADZAzC+LZ+mo2tIX4H6uZu0ulrVmG5Tqz0=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 h1:gzB4c6ztb38C/jYiqEaFC+mCGcWFHDji9e6jwymY9d4=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2/go.mod h1:l1qIPIq2uRV5WTSvkbhbl/ndbeOu7OCb3UZ+0+2ZSb8=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc=
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
github.com/nrdcg/vegadns v0.3.0 h1:11FQMw7xVIRUWO9o5+Z/5YZhmPWlm4oxUUH3F6EVqQU=
@ -904,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.28 h1:Rj1WXXNPm9AsPf0PJhWCvlsqfcKPUYdyVnkmEc3O8sI=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.24/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.38/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 h1:bCs+z6dxRaHWm/C1D/XkSOcCZ0+W2+/6HmIXjpAj+fY=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
@ -922,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.233 h1:Hh2pzwu/Wq19rsZgNo3HdpjQB28D/F0+m6EjLVggmhM=
github.com/volcengine/volc-sdk-golang v1.0.233/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/volcengine/volc-sdk-golang v1.0.237 h1:hpLKiS2BwDcSBtZWSz034foCbd0h3FrHTKlUMqHIdc4=
github.com/volcengine/volc-sdk-golang v1.0.237/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
@ -934,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.43.0 h1:HjBesEmCN8ZOhjjh8gs605vvi9/MBJAW3P20OJ4iQnw=
github.com/yandex-cloud/go-genproto v0.43.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
github.com/yandex-cloud/go-sdk/services/dns v0.0.25 h1:BcGEuOnwq2X3LS2kvFC6BOdZkOq4Lc7XAYvzap/SJJY=
github.com/yandex-cloud/go-sdk/services/dns v0.0.25/go.mod h1:B4QHijALUHIjRxL3aqmOwDrHYUI2XdeeG4WKItth3jI=
github.com/yandex-cloud/go-sdk/v2 v2.37.0 h1:WvttW6p9xcWag9j+GQv+GJXPggggXGwOlIJNfkWmFWw=
github.com/yandex-cloud/go-sdk/v2 v2.37.0/go.mod h1:Dt4a81enjRsm4xMJyW5E1Y/vaUYwXJvUGRdDLuM2k6I=
github.com/yandex-cloud/go-genproto v0.54.0 h1:LjEwDPBAtF39HvcPQe8I+ImCnFasCPCOVh2b2Sr2eAg=
github.com/yandex-cloud/go-genproto v0.54.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
github.com/yandex-cloud/go-sdk/services/dns v0.0.36 h1:sD622+baDvJ2ujhCfoFsCH0XeNsaZNW6loRqvRavjtE=
github.com/yandex-cloud/go-sdk/services/dns v0.0.36/go.mod h1:Hh7IKJxULaRzmyM19lQZw+yUDyMM8M3Qrk1LbWqhCkc=
github.com/yandex-cloud/go-sdk/v2 v2.56.0 h1:rihPAZbPbHU/BKTLuT64nU1uhbBrO20HhdlLR3Hyoz0=
github.com/yandex-cloud/go-sdk/v2 v2.56.0/go.mod h1:jzVBQgamNHoiDsmjog2dPZHMXuGZqmxf/epH+Qb7Emc=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
@ -969,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=
@ -1031,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=
@ -1076,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=
@ -1135,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=
@ -1248,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=
@ -1264,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=
@ -1284,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=
@ -1351,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=
@ -1381,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.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ=
google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4=
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -1421,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-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -1475,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=

View file

@ -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) {

View file

@ -23,6 +23,8 @@ lego --dns alidns - -d '*.example.com' -d example.com run
ALICLOUD_SECRET_KEY = "Access Key secret"
ALICLOUD_SECURITY_TOKEN = "STS Security Token (optional)"
[Configuration.Additional]
ALICLOUD_REGION_ID = "Region ID (Default: cn-hangzhou)"
ALICLOUD_LINE = "Line (Default: default)"
ALICLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
ALICLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
ALICLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)"

View file

@ -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,11 +122,6 @@ 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)
@ -135,6 +131,11 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
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)
@ -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)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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)

View file

@ -0,0 +1,7 @@
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
<Body>
<KasAuth xmlns="https://kasserver.com/">
<Params>{"kas_login":"user","kas_auth_data":"secret","kas_auth_type":"plain","session_lifetime":60,"session_update_lifetime":"Y"}</Params>
</KasAuth>
</Body>
</Envelope>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>SOAP-ENV:Server</faultcode>
<faultstring>flood_protection</faultstring>
<faultactor>KasApi</faultactor>
<detail>0.0688529014587</detail>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>SOAP-ENV:Server</faultcode>
<faultstring>zone_not_found</faultstring>
<faultactor>KasApi</faultactor>
<detail>example.com</detail>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>SOAP-ENV:Server</faultcode>
<faultstring>zone_syntax_incorrect</faultstring>
<faultactor>KasApi</faultactor>
<detail>_acme-challenge.example.com</detail>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

View file

@ -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)
}

View file

@ -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)

View file

@ -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.

View file

@ -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"`

View file

@ -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
}

View file

@ -0,0 +1,24 @@
Name = "ArtFiles"
Description = ''''''
URL = "https://www.artfiles.de/extras/domains/"
Code = "artfiles"
Since = "v4.32.0"
Example = '''
ARTFILES_USERNAME="xxx" \
ARTFILES_PASSWORD="yyy" \
lego --dns artfiles -d '*.example.com' -d example.com run
'''
[Configuration]
[Configuration.Credentials]
ARTFILES_USERNAME = "API username"
ARTFILES_PASSWORD = "API password"
[Configuration.Additional]
ARTFILES_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
ARTFILES_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 360)"
ARTFILES_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
ARTFILES_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
[Links]
API = "https://support.artfiles.de/DCP-API#dns"

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -0,0 +1,3 @@
example.com normal 2026-10-01 2017-09-18 163477
example.org normal 2026-08-01 2016-07-07 156216
example.net normal 2026-07-01 2017-06-06 162462

View file

@ -0,0 +1,16 @@
{
"data": {
"SRV": "_imap._tcp 0 0 0 .\n_imaps._tcp 0 1 993 mail.example.tld.\n_pop3._tcp 0 0 0 .\n_pop3s._tcp 0 0 0 .",
"AAAA": "",
"MX": "10 mail.example.tld.",
"CAA": "@ 128 iodef \"mailto:someone@example.tld\"\n@ 128 issue \"letsencrypt.org\"\n@ 128 issuewild \"letsencrypt.org\"",
"TTL": 3600,
"comment": "TLSA RR:\nInfo -> https://dnssec-stats.ant.isi.edu/~viktor/x3hosts.html\nTest 1 -> https://stats.dnssec-tools.org/explore/?example.tld\nTest 2 -> https://dane.sys4.de/smtp/example.tld\n\nSMIMEA RR:\nGenerator -> https://www.smimea.info/smimea-generator.php\nTest -> https://www.smimea.info/smimea-test.php",
"TXT": "_dmarc \"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\"\n_mta-sts \"v=STSv1;id=yyyymmddTHHMMSS;\"\n_smtp._tls \"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\"\n@ \"v=spf1 a mx ~all\"\nselector._domainkey \"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\"\nselectorecc._domainkey \"v=DKIM1;k=ed25519;p=Base64Stuff\"\n_acme-challenge \"TheAcmeChallenge\"",
"A": "sub1 1.2.3.4\nsub2 1.2.3.4\nsub3 1.2.3.4\nsub4 1.2.3.4\nsub5 1.2.3.4\nsub6 1.2.3.4\nsub7 1.2.3.4\nsub8 1.2.3.4\nsub9 1.2.3.4\nsub10 1.2.3.4\nsub11 1.2.3.4\nsub12 1.2.3.4\nsub13 1.2.3.4\nsub14 1.2.3.4\nsub15 1.2.3.4\nsub16 1.2.3.4\nsub17 1.2.3.4\nsub18 1.2.3.4\n@ 1.2.3.4",
"nameserver": "auth1.artfiles.de.\nauth2.artfiles.de.",
"CName": "some cname.to.example.tld.",
"TLSA": "_25._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_25._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_25._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_465._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_465._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_465._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_587._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_587._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_587._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2"
},
"status": "OK"
}

View file

@ -0,0 +1,4 @@
{
"status": "OK",
"error": ""
}

View file

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

View file

@ -0,0 +1,7 @@
_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf"
_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;"
_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com"
@ "v=spf1 a mx ~all"
selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff"
selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff"
_acme-challenge "TheAcmeChallenge"

View file

@ -0,0 +1,109 @@
package internal
import (
"encoding/csv"
"encoding/json"
"errors"
"io"
"maps"
"slices"
"strconv"
"strings"
"unicode"
)
type Records struct {
Data map[string]json.RawMessage `json:"data"`
Status string `json:"status"`
}
type RecordValue map[string][]string
func (r RecordValue) Set(key, value string) {
r[key] = []string{strconv.Quote(value)}
}
func (r RecordValue) Add(key, value string) {
r[key] = append(r[key], strconv.Quote(value))
}
func (r RecordValue) Delete(key string) {
delete(r, key)
}
func (r RecordValue) RemoveValue(key, value string) {
if len(r[key]) == 0 {
return
}
quotedValue := strconv.Quote(value)
var data []string
for _, s := range r[key] {
if s != quotedValue {
data = append(data, s)
}
}
r[key] = data
if len(r[key]) == 0 {
r.Delete(key)
}
}
func (r RecordValue) String() string {
var parts []string
for _, key := range slices.Sorted(maps.Keys(r)) {
for _, s := range r[key] {
parts = append(parts, key+" "+s)
}
}
return strings.Join(parts, "\n")
}
func ParseRecordValue(lines string) RecordValue {
data := make(RecordValue)
for line := range strings.Lines(lines) {
line = strings.TrimSpace(line)
idx := strings.IndexFunc(line, unicode.IsSpace)
data[line[:idx]] = append(data[line[:idx]], line[idx+1:])
}
return data
}
func parseDomains(input string) ([]string, error) {
reader := csv.NewReader(strings.NewReader(input))
reader.Comma = '\t'
reader.TrimLeadingSpace = true
reader.LazyQuotes = true
var data []string
for {
record, err := reader.Read()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
if len(record) < 1 {
// Malformed line
continue
}
data = append(data, record[0])
}
return data, nil
}

View file

@ -0,0 +1,183 @@
package internal
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRecordValue_Set(t *testing.T) {
rv := make(RecordValue)
rv.Set("a", "1")
rv.Set("b", "2")
rv.Set("b", "3")
assert.Equal(t, "a \"1\"\nb \"3\"", rv.String())
}
func TestRecordValue_Add(t *testing.T) {
rv := make(RecordValue)
rv.Add("a", "1")
rv.Add("b", "2")
rv.Add("b", "3")
assert.Equal(t, "a \"1\"\nb \"2\"\nb \"3\"", rv.String())
}
func TestRecordValue_Delete(t *testing.T) {
rv := make(RecordValue)
rv.Set("a", "1")
rv.Add("b", "2")
rv.Delete("b")
assert.Equal(t, "a \"1\"", rv.String())
}
func TestRecordValue_RemoveValue(t *testing.T) {
testCases := []struct {
desc string
data map[string][]string
toRemove map[string][]string
expected string
}{
{
desc: "remove the only value",
data: map[string][]string{
"a": {"1"},
},
toRemove: map[string][]string{
"a": {"1"},
},
expected: ``,
},
{
desc: "remove value in the middle",
data: map[string][]string{
"a": {"1", "2", "3"},
},
toRemove: map[string][]string{
"a": {"2"},
},
expected: "a \"1\"\na \"3\"",
},
{
desc: "remove value at the beginning",
data: map[string][]string{
"a": {"1", "2", "3"},
},
toRemove: map[string][]string{
"a": {"1"},
},
expected: "a \"2\"\na \"3\"",
},
{
desc: "remove value at the end",
data: map[string][]string{
"a": {"1", "2", "3"},
},
toRemove: map[string][]string{
"a": {"3"},
},
expected: "a \"1\"\na \"2\"",
},
{
desc: "remove all (delete)",
data: map[string][]string{
"a": {"1", "2", "3"},
},
toRemove: map[string][]string{
"a": {"1", "2", "3"},
},
expected: ``,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rv := make(RecordValue)
for k, values := range test.data {
for _, v := range values {
rv.Add(k, v)
}
}
for k, values := range test.toRemove {
for _, v := range values {
rv.RemoveValue(k, v)
}
}
assert.Equal(t, test.expected, rv.String())
})
}
}
func TestParseRecordValue(t *testing.T) {
testCases := []struct {
desc string
filename string
expected RecordValue
}{
{
desc: "simple",
filename: "txt_record.txt",
expected: RecordValue{
"@": []string{"\"v=spf1 a mx ~all\""},
"_acme-challenge": []string{"\"TheAcmeChallenge\""},
"_dmarc": []string{"\"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\""},
"_mta-sts": []string{"\"v=STSv1;id=yyyymmddTHHMMSS;\""},
"_smtp._tls": []string{"\"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\""},
"selector._domainkey": []string{"\"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\""},
"selectorecc._domainkey": []string{"\"v=DKIM1;k=ed25519;p=Base64Stuff\""},
},
},
{
desc: "multiple values with the same key",
filename: "txt_record-multiple.txt",
expected: RecordValue{
"@": []string{"\"v=spf1 a mx ~all\""},
"_acme-challenge": []string{"\"xxx\"", "\"yyy\""},
"_dmarc": []string{"\"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\""},
"_mta-sts": []string{"\"v=STSv1;id=yyyymmddTHHMMSS;\""},
"_smtp._tls": []string{"\"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\""},
"selector._domainkey": []string{"\"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\""},
"selectorecc._domainkey": []string{"\"v=DKIM1;k=ed25519;p=Base64Stuff\""},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
file, err := os.ReadFile(filepath.Join("fixtures", test.filename))
require.NoError(t, err)
data := ParseRecordValue(string(file))
assert.Equal(t, test.expected, data)
})
}
}
func Test_parseDomains(t *testing.T) {
file, err := os.ReadFile(filepath.FromSlash("./fixtures/domains.txt"))
require.NoError(t, err)
domains, err := parseDomains(string(file))
require.NoError(t, err)
expected := []string{"example.com", "example.org", "example.net"}
assert.Equal(t, expected, domains)
}

View file

@ -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}
}

View file

@ -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()

View file

@ -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()

View file

@ -0,0 +1,249 @@
// Package bluecatv2 implements a DNS provider for solving the DNS-01 challenge using Bluecat v2.
package bluecatv2
import (
"context"
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/go-acme/lego/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)
}

View file

@ -0,0 +1,33 @@
Name = "Bluecat v2"
Description = ''''''
URL = "https://www.bluecatnetworks.com"
Code = "bluecatv2"
Since = "v4.32.0"
Example = '''
BLUECATV2_SERVER_URL="https://example.com" \
BLUECATV2_USERNAME="xxx" \
BLUECATV2_PASSWORD="yyy" \
BLUECATV2_CONFIG_NAME="myConfiguration" \
BLUECATV2_VIEW_NAME="myView" \
lego --dns bluecatv2 -d '*.example.com' -d example.com run
'''
[Configuration]
[Configuration.Credentials]
BLUECAT_SERVER_URL = "The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve"
BLUECATV2_USERNAME = "API username"
BLUECATV2_PASSWORD = "API password"
BLUECATV2_CONFIG_NAME = "Configuration name"
BLUECATV2_VIEW_NAME = "DNS View Name"
[Configuration.Additional]
BLUECATV2_SKIP_DEPLOY = "Skip quick deployements"
BLUECATV2_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
BLUECATV2_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
BLUECATV2_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
BLUECATV2_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
[Links]
API = "https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Introduction/9.6.0"
Swagger = "http://{Address_Manager_IP}/api/openapi.json"
SwaggerDump = "https://github.com/go-acme/lego/discussions/2218#discussioncomment-13060545"

View file

@ -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)
}

View file

@ -0,0 +1,221 @@
package internal
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
"github.com/go-acme/lego/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
}

View file

@ -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)
}

View file

@ -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."
}

View file

@ -0,0 +1,6 @@
{
"status": 401,
"reason": "Unauthorized",
"code": "InvalidAuthorizationToken",
"message": "The provided authorization token is invalid"
}

View file

@ -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"
}
]
}

View file

@ -0,0 +1,4 @@
{
"username": "userA",
"password": "secret"
}

View file

@ -0,0 +1,50 @@
{
"id": 12345,
"type": "UserSession",
"apiToken": "VZoO2Z0BjBaJyvuhE4vNJRWqI9upwDHk70UNi0Ez",
"apiTokenExpirationDateTime": "2022-09-15T17:52:07Z",
"basicAuthenticationCredentials": "YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=",
"remoteAddress": "192.168.1.1",
"readOnly": true,
"loginDateTime": "2022-09-14T17:45:03Z",
"logoutDateTime": "2022-09-14T19:45:03Z",
"state": "LOGGED_IN",
"response": "Authentication Error: Ensure that your username and password are correct.",
"user": {
"id": 103307,
"type": "User",
"name": "admin",
"userDefinedFields": {
"udf1": "value1",
"udf2": "value2"
},
"authenticator": {
"id": 12345,
"type": "Authenticator",
"name": "LDAP authenticator"
},
"email": "user@example.com",
"phoneNumber": "555-1234",
"securityPrivilege": "NO_ACCESS",
"historyPrivilege": "HIDE",
"accessType": "GUI",
"passwordResetRequired": true,
"accountLocked": true,
"x509Required": true,
"administrativeAccessRights": [
{
"resourceType": "Event",
"accessLevel": "HIDE"
}
]
},
"authenticator": {
"id": 12345,
"type": "Authenticator",
"name": "LDAP authenticator",
"userDefinedFields": {
"udf1": "value1",
"udf2": "value2"
}
}
}

View file

@ -0,0 +1,3 @@
{
"type": "QuickDeployment"
}

View file

@ -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"
}

View file

@ -0,0 +1,7 @@
{
"type": "TXTRecord",
"name": "_acme-challenge",
"ttl": 120,
"recordType": "TXT",
"text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"
}

View file

@ -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"
}
}

View file

@ -0,0 +1,185 @@
{
"count": 0,
"totalCount": 0,
"data": [
{
"id": 12345,
"type": "ENUMZone",
"name": "5678",
"userDefinedFields": {
"udf1": "value1",
"udf2": "value2"
},
"configuration": {
"id": 12345,
"type": "Configuration",
"name": "name"
},
"view": {
"id": 12345,
"type": "View",
"name": "default",
"userDefinedFields": {
"udf1": "value1",
"udf2": "value2"
},
"configuration": {
"id": 12345,
"type": "Configuration",
"name": "name"
},
"deviceRegistrationEnabled": true,
"deviceRegistrationPortalAddress": "10.10.10.10"
},
"deploymentEnabled": true,
"absoluteName": "string"
},
{
"id": 12345,
"type": "ExternalHostsZone",
"name": "name",
"userDefinedFields": {
"udf1": "value1",
"udf2": "value2"
},
"configuration": {
"id": 12345,
"type": "Configuration",
"name": "name"
},
"view": {
"id": 12345,
"type": "View",
"name": "default",
"userDefinedFields": {
"udf1": "value1",
"udf2": "value2"
},
"configuration": {
"id": 12345,
"type": "Configuration",
"name": "name"
},
"deviceRegistrationEnabled": true,
"deviceRegistrationPortalAddress": "10.10.10.10"
}
},
{
"id": 12345,
"type": "InternalRootZone",
"name": "name",
"userDefinedFields": {
"udf1": "value1",
"udf2": "value2"
},
"configuration": {
"id": 12345,
"type": "Configuration",
"name": "name"
},
"view": {
"id": 12345,
"type": "View",
"name": "default",
"userDefinedFields": {
"udf1": "value1",
"udf2": "value2"
},
"configuration": {
"id": 12345,
"type": "Configuration",
"name": "name"
},
"deviceRegistrationEnabled": true,
"deviceRegistrationPortalAddress": "10.10.10.10"
},
"deploymentEnabled": true
},
{
"id": 12345,
"type": "ResponsePolicyZone",
"name": "name",
"userDefinedFields": {
"udf1": "value1",
"udf2": "value2"
},
"configuration": {
"id": 12345,
"type": "Configuration",
"name": "name"
},
"view": {
"id": 12345,
"type": "View",
"name": "default",
"userDefinedFields": {
"udf1": "value1",
"udf2": "value2"
},
"configuration": {
"id": 12345,
"type": "Configuration",
"name": "name"
},
"deviceRegistrationEnabled": true,
"deviceRegistrationPortalAddress": "10.10.10.10"
},
"responsePolicyZoneType": "LOCAL",
"responsePolicy": {
"id": 12345,
"type": "ResponsePolicy",
"name": "Block Response Policy"
},
"overridePolicyType": "ALLOWLIST",
"overrideRefreshTime": "string",
"redirectTarget": "string",
"feedCategories": [
"string"
]
},
{
"id": 12345,
"type": "Zone",
"name": "example.com",
"userDefinedFields": {
"udf1": "value1",
"udf2": "value2"
},
"configuration": {
"id": 12345,
"type": "Configuration",
"name": "name"
},
"view": {
"id": 12345,
"type": "View",
"name": "default",
"userDefinedFields": {
"udf1": "value1",
"udf2": "value2"
},
"configuration": {
"id": 12345,
"type": "Configuration",
"name": "name"
},
"deviceRegistrationEnabled": true,
"deviceRegistrationPortalAddress": "10.10.10.10"
},
"deploymentEnabled": true,
"dynamicUpdateEnabled": true,
"template": {
"id": 12345,
"type": "ZoneTemplate",
"name": "name"
},
"signed": true,
"signingPolicy": {
"id": 12345,
"type": "DNSSECSigningPolicy",
"name": "name"
},
"absoluteName": "example.com"
}
]
}

View file

@ -0,0 +1,60 @@
package internal
import (
"context"
"fmt"
"net/http"
)
type token string
const tokenKey token = "token"
const authorizationHeader = "Authorization"
// CreateSession creates a new session.
func (c *Client) CreateSession(ctx context.Context, info LoginInfo) (*Session, error) {
endpoint := c.baseURL.JoinPath("api", "v2", "sessions")
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, info)
if err != nil {
return nil, err
}
result := new(Session)
err = c.do(req, result)
if err != nil {
return nil, err
}
return result, nil
}
// CreateAuthenticatedContext creates a new authenticated context.
func (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) {
tok, err := c.CreateSession(ctx, LoginInfo{Username: c.username, Password: c.password})
if err != nil {
return nil, fmt.Errorf("create session: %w", err)
}
return context.WithValue(ctx, tokenKey, tok.BasicAuthenticationCredentials), nil
}
func (c *Client) doAuthenticated(ctx context.Context, req *http.Request, result any) error {
tok := getToken(ctx)
if tok != "" {
req.Header.Set(authorizationHeader, "Basic "+tok)
}
return c.do(req, result)
}
func getToken(ctx context.Context) string {
tok, ok := ctx.Value(tokenKey).(string)
if !ok {
return ""
}
return tok
}

View file

@ -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))
}

View file

@ -0,0 +1,64 @@
package internal
import (
"fmt"
"strings"
)
type Predicate struct {
field string
operator string
values []string
}
func (p *Predicate) String() string {
var values []string
for _, v := range p.values {
values = append(values, fmt.Sprintf("'%s'", v))
}
return fmt.Sprintf("%s:%s(%s)", p.field, p.operator, strings.Join(values, ", "))
}
func Eq(field, value string) *Predicate {
return &Predicate{field: field, operator: "eq", values: []string{value}}
}
func Contains(field, value string) *Predicate {
return &Predicate{field: field, operator: "contains", values: []string{value}}
}
func StartsWith(field, value string) *Predicate {
return &Predicate{field: field, operator: "startsWith", values: []string{value}}
}
func EndsWith(field, value string) *Predicate {
return &Predicate{field: field, operator: "endsWith", values: []string{value}}
}
func In(field string, values ...string) *Predicate {
return &Predicate{field: field, operator: "in", values: values}
}
type Combined struct {
predicates []*Predicate
operator string
}
func (o *Combined) String() string {
var parts []string
for _, predicate := range o.predicates {
parts = append(parts, predicate.String())
}
return strings.Join(parts, " "+o.operator+" ")
}
func And(predicates ...*Predicate) *Combined {
return &Combined{predicates: predicates, operator: "and"}
}
func Or(predicates ...*Predicate) *Combined {
return &Combined{predicates: predicates, operator: "or"}
}

View file

@ -0,0 +1,78 @@
package internal
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPredicate(t *testing.T) {
testCases := []struct {
desc string
predicate fmt.Stringer
expected string
}{
{
desc: "Equals",
predicate: Eq("foo", "bar"),
expected: "foo:eq('bar')",
},
{
desc: "Contains",
predicate: Contains("foo", "bar"),
expected: "foo:contains('bar')",
},
{
desc: "Starts with",
predicate: StartsWith("foo", "bar"),
expected: "foo:startsWith('bar')",
},
{
desc: "Ends with",
predicate: EndsWith("foo", "bar"),
expected: "foo:endsWith('bar')",
},
{
desc: "Match a list of values",
predicate: In("foo", "bar", "bir"),
expected: "foo:in('bar', 'bir')",
},
{
desc: "Combined: and",
predicate: And(Eq("foo", "bar"), Eq("fii", "bir")),
expected: "foo:eq('bar') and fii:eq('bir')",
},
{
desc: "Combined: multiple and",
predicate: And(
Eq("foo", "bar"),
Eq("fii", "bir"),
Eq("fuu", "bur"),
),
expected: "foo:eq('bar') and fii:eq('bir') and fuu:eq('bur')",
},
{
desc: "Combined: or",
predicate: Or(Eq("foo", "bar"), Eq("foo", "bir")),
expected: "foo:eq('bar') or foo:eq('bir')",
},
{
desc: "Combined: multiple or",
predicate: Or(
Eq("foo", "bar"),
Eq("foo", "bir"),
Eq("foo", "bur"),
),
expected: "foo:eq('bar') or foo:eq('bir') or foo:eq('bur')",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
assert.Equal(t, test.expected, test.predicate.String())
})
}
}

View file

@ -0,0 +1,122 @@
package internal
import (
"fmt"
"time"
)
// Quick deployment states.
//
//nolint:misspell // US vs UK
const (
QDStatePending = "PENDING"
QDStateQueued = "QUEUED"
QDStateRunning = "RUNNING"
QDStateCancelled = "CANCELLED"
QDStateCancelling = "CANCELLING"
QDStateCompleted = "COMPLETED"
QDStateCompletedWithErrors = "COMPLETED_WITH_ERRORS"
QDStateCompletedWithWarnings = "COMPLETED_WITH_WARNINGS"
QDStateFailed = "FAILED"
QDStateUnknown = "UNKNOWN"
)
// APIError represents an error.
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Errors/9.6.0
type APIError struct {
Status int `json:"status"`
Reason string `json:"reason"`
Code string `json:"code"`
Message string `json:"message"`
}
func (a *APIError) Error() string {
return fmt.Sprintf("%d: %s: %s: %s", a.Status, a.Reason, a.Code, a.Message)
}
// CommonResource represents the common resource fields.
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Resources/9.6.0
type CommonResource struct {
ID int64 `json:"id,omitempty"`
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
}
// Collection represents a collection of resources.
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Collections/9.6.0
type Collection[T any] struct {
Count int64 `json:"count"`
TotalCount int64 `json:"totalCount"`
Data []T `json:"data"`
}
type CollectionOptions struct {
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Fields/9.6.0
Fields string `url:"fields,omitempty"`
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Pagination/9.6.0
Limit int `url:"limit,omitempty"`
Offset int `url:"offset,omitempty"`
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Filter/9.6.0
Filter string `url:"filter,omitempty"`
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Ordering/9.6.0
OrderBy string `url:"orderBy,omitempty"`
// Should return or not the total number of resources matching the query.
Total bool `url:"total,omitempty"`
}
type RecordTXT struct {
CommonResource
TTL int `json:"ttl,omitempty"`
AbsoluteName string `json:"absoluteName,omitempty"`
Comment string `json:"comment,omitempty"`
Dynamic bool `json:"dynamic,omitempty"`
RecordType string `json:"recordType,omitempty"`
Text string `json:"text,omitempty"`
}
type ZoneResource struct {
CommonResource
AbsoluteName string `json:"absoluteName,omitempty"`
}
type QuickDeployment struct {
CommonResource
State string `json:"state,omitempty"`
Status string `json:"status,omitempty"`
Message string `json:"message,omitempty"`
PercentComplete int `json:"percentComplete,omitempty"`
CreationDateTime time.Time `json:"creationDateTime,omitzero"`
StartDateTime time.Time `json:"startDateTime,omitzero"`
CompletionDateTime time.Time `json:"completionDateTime,omitzero"`
Method string `json:"method,omitempty"`
}
// LoginInfo represents the login information.
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Creating-an-API-session/9.6.0
type LoginInfo struct {
Username string `json:"username"`
Password string `json:"password"`
}
// Session represents the session.
// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Creating-an-API-session/9.6.0
type Session struct {
ID int `json:"id"`
Type string `json:"type"`
APIToken string `json:"apiToken"`
APITokenExpirationDateTime time.Time `json:"apiTokenExpirationDateTime"`
BasicAuthenticationCredentials string `json:"basicAuthenticationCredentials"`
RemoteAddress string `json:"remoteAddress"`
ReadOnly bool `json:"readOnly"`
LoginDateTime time.Time `json:"loginDateTime"`
LogoutDateTime time.Time `json:"logoutDateTime"`
State string `json:"state"`
Response string `json:"response"`
}

View file

@ -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)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
}

View file

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

Some files were not shown because too many files have changed in this diff Show more