Add DNS provider for CPanel and WHM (#1977)
This commit is contained in:
parent
719adc3964
commit
83ff393131
25 changed files with 1778 additions and 28 deletions
|
@ -237,3 +237,5 @@ issues:
|
||||||
text: 'Duplicate words \(0\) found'
|
text: 'Duplicate words \(0\) found'
|
||||||
- path: cmd/cmd_renew.go
|
- path: cmd/cmd_renew.go
|
||||||
text: 'cyclomatic complexity 15 of func `renewForDomains` is high'
|
text: 'cyclomatic complexity 15 of func `renewForDomains` is high'
|
||||||
|
- path: providers/dns/cpanel/cpanel.go
|
||||||
|
text: 'cyclomatic complexity 13 of func `\(\*DNSProvider\)\.CleanUp` is high'
|
||||||
|
|
54
README.md
54
README.md
|
@ -58,33 +58,33 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
|
||||||
| [Azure (deprecated)](https://go-acme.github.io/lego/dns/azure/) | [Azure DNS](https://go-acme.github.io/lego/dns/azuredns/) | [Bindman](https://go-acme.github.io/lego/dns/bindman/) | [Bluecat](https://go-acme.github.io/lego/dns/bluecat/) |
|
| [Azure (deprecated)](https://go-acme.github.io/lego/dns/azure/) | [Azure DNS](https://go-acme.github.io/lego/dns/azuredns/) | [Bindman](https://go-acme.github.io/lego/dns/bindman/) | [Bluecat](https://go-acme.github.io/lego/dns/bluecat/) |
|
||||||
| [Brandit](https://go-acme.github.io/lego/dns/brandit/) | [Bunny](https://go-acme.github.io/lego/dns/bunny/) | [Checkdomain](https://go-acme.github.io/lego/dns/checkdomain/) | [Civo](https://go-acme.github.io/lego/dns/civo/) |
|
| [Brandit](https://go-acme.github.io/lego/dns/brandit/) | [Bunny](https://go-acme.github.io/lego/dns/bunny/) | [Checkdomain](https://go-acme.github.io/lego/dns/checkdomain/) | [Civo](https://go-acme.github.io/lego/dns/civo/) |
|
||||||
| [Cloud.ru](https://go-acme.github.io/lego/dns/cloudru/) | [CloudDNS](https://go-acme.github.io/lego/dns/clouddns/) | [Cloudflare](https://go-acme.github.io/lego/dns/cloudflare/) | [ClouDNS](https://go-acme.github.io/lego/dns/cloudns/) |
|
| [Cloud.ru](https://go-acme.github.io/lego/dns/cloudru/) | [CloudDNS](https://go-acme.github.io/lego/dns/clouddns/) | [Cloudflare](https://go-acme.github.io/lego/dns/cloudflare/) | [ClouDNS](https://go-acme.github.io/lego/dns/cloudns/) |
|
||||||
| [CloudXNS](https://go-acme.github.io/lego/dns/cloudxns/) | [ConoHa](https://go-acme.github.io/lego/dns/conoha/) | [Constellix](https://go-acme.github.io/lego/dns/constellix/) | [Derak Cloud](https://go-acme.github.io/lego/dns/derak/) |
|
| [CloudXNS](https://go-acme.github.io/lego/dns/cloudxns/) | [ConoHa](https://go-acme.github.io/lego/dns/conoha/) | [Constellix](https://go-acme.github.io/lego/dns/constellix/) | [CPanel/WHM](https://go-acme.github.io/lego/dns/cpanel/) |
|
||||||
| [deSEC.io](https://go-acme.github.io/lego/dns/desec/) | [Designate DNSaaS for Openstack](https://go-acme.github.io/lego/dns/designate/) | [Digital Ocean](https://go-acme.github.io/lego/dns/digitalocean/) | [DNS Made Easy](https://go-acme.github.io/lego/dns/dnsmadeeasy/) |
|
| [Derak Cloud](https://go-acme.github.io/lego/dns/derak/) | [deSEC.io](https://go-acme.github.io/lego/dns/desec/) | [Designate DNSaaS for Openstack](https://go-acme.github.io/lego/dns/designate/) | [Digital Ocean](https://go-acme.github.io/lego/dns/digitalocean/) |
|
||||||
| [dnsHome.de](https://go-acme.github.io/lego/dns/dnshomede/) | [DNSimple](https://go-acme.github.io/lego/dns/dnsimple/) | [DNSPod (deprecated)](https://go-acme.github.io/lego/dns/dnspod/) | [Domain Offensive (do.de)](https://go-acme.github.io/lego/dns/dode/) |
|
| [DNS Made Easy](https://go-acme.github.io/lego/dns/dnsmadeeasy/) | [dnsHome.de](https://go-acme.github.io/lego/dns/dnshomede/) | [DNSimple](https://go-acme.github.io/lego/dns/dnsimple/) | [DNSPod (deprecated)](https://go-acme.github.io/lego/dns/dnspod/) |
|
||||||
| [Domeneshop](https://go-acme.github.io/lego/dns/domeneshop/) | [DreamHost](https://go-acme.github.io/lego/dns/dreamhost/) | [Duck DNS](https://go-acme.github.io/lego/dns/duckdns/) | [Dyn](https://go-acme.github.io/lego/dns/dyn/) |
|
| [Domain Offensive (do.de)](https://go-acme.github.io/lego/dns/dode/) | [Domeneshop](https://go-acme.github.io/lego/dns/domeneshop/) | [DreamHost](https://go-acme.github.io/lego/dns/dreamhost/) | [Duck DNS](https://go-acme.github.io/lego/dns/duckdns/) |
|
||||||
| [Dynu](https://go-acme.github.io/lego/dns/dynu/) | [EasyDNS](https://go-acme.github.io/lego/dns/easydns/) | [Efficient IP](https://go-acme.github.io/lego/dns/efficientip/) | [Epik](https://go-acme.github.io/lego/dns/epik/) |
|
| [Dyn](https://go-acme.github.io/lego/dns/dyn/) | [Dynu](https://go-acme.github.io/lego/dns/dynu/) | [EasyDNS](https://go-acme.github.io/lego/dns/easydns/) | [Efficient IP](https://go-acme.github.io/lego/dns/efficientip/) |
|
||||||
| [Exoscale](https://go-acme.github.io/lego/dns/exoscale/) | [External program](https://go-acme.github.io/lego/dns/exec/) | [freemyip.com](https://go-acme.github.io/lego/dns/freemyip/) | [G-Core](https://go-acme.github.io/lego/dns/gcore/) |
|
| [Epik](https://go-acme.github.io/lego/dns/epik/) | [Exoscale](https://go-acme.github.io/lego/dns/exoscale/) | [External program](https://go-acme.github.io/lego/dns/exec/) | [freemyip.com](https://go-acme.github.io/lego/dns/freemyip/) |
|
||||||
| [Gandi Live DNS (v5)](https://go-acme.github.io/lego/dns/gandiv5/) | [Gandi](https://go-acme.github.io/lego/dns/gandi/) | [Glesys](https://go-acme.github.io/lego/dns/glesys/) | [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) |
|
| [G-Core](https://go-acme.github.io/lego/dns/gcore/) | [Gandi Live DNS (v5)](https://go-acme.github.io/lego/dns/gandiv5/) | [Gandi](https://go-acme.github.io/lego/dns/gandi/) | [Glesys](https://go-acme.github.io/lego/dns/glesys/) |
|
||||||
| [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) | [Google Domains](https://go-acme.github.io/lego/dns/googledomains/) | [Hetzner](https://go-acme.github.io/lego/dns/hetzner/) | [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) |
|
| [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) | [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) | [Google Domains](https://go-acme.github.io/lego/dns/googledomains/) | [Hetzner](https://go-acme.github.io/lego/dns/hetzner/) |
|
||||||
| [Hosttech](https://go-acme.github.io/lego/dns/hosttech/) | [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | [http.net](https://go-acme.github.io/lego/dns/httpnet/) | [Hurricane Electric DNS](https://go-acme.github.io/lego/dns/hurricane/) |
|
| [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) | [Hosttech](https://go-acme.github.io/lego/dns/hosttech/) | [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | [http.net](https://go-acme.github.io/lego/dns/httpnet/) |
|
||||||
| [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) | [IBM Cloud (SoftLayer)](https://go-acme.github.io/lego/dns/ibmcloud/) | [IIJ DNS Platform Service](https://go-acme.github.io/lego/dns/iijdpf/) | [Infoblox](https://go-acme.github.io/lego/dns/infoblox/) |
|
| [Hurricane Electric DNS](https://go-acme.github.io/lego/dns/hurricane/) | [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) | [IBM Cloud (SoftLayer)](https://go-acme.github.io/lego/dns/ibmcloud/) | [IIJ DNS Platform Service](https://go-acme.github.io/lego/dns/iijdpf/) |
|
||||||
| [Infomaniak](https://go-acme.github.io/lego/dns/infomaniak/) | [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [Internet.bs](https://go-acme.github.io/lego/dns/internetbs/) | [INWX](https://go-acme.github.io/lego/dns/inwx/) |
|
| [Infoblox](https://go-acme.github.io/lego/dns/infoblox/) | [Infomaniak](https://go-acme.github.io/lego/dns/infomaniak/) | [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [Internet.bs](https://go-acme.github.io/lego/dns/internetbs/) |
|
||||||
| [Ionos](https://go-acme.github.io/lego/dns/ionos/) | [IPv64](https://go-acme.github.io/lego/dns/ipv64/) | [iwantmyname](https://go-acme.github.io/lego/dns/iwantmyname/) | [Joker](https://go-acme.github.io/lego/dns/joker/) |
|
| [INWX](https://go-acme.github.io/lego/dns/inwx/) | [Ionos](https://go-acme.github.io/lego/dns/ionos/) | [IPv64](https://go-acme.github.io/lego/dns/ipv64/) | [iwantmyname](https://go-acme.github.io/lego/dns/iwantmyname/) |
|
||||||
| [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Liara](https://go-acme.github.io/lego/dns/liara/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) | [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) |
|
| [Joker](https://go-acme.github.io/lego/dns/joker/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Liara](https://go-acme.github.io/lego/dns/liara/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) |
|
||||||
| [Loopia](https://go-acme.github.io/lego/dns/loopia/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Manual](https://go-acme.github.io/lego/dns/manual/) | [Metaname](https://go-acme.github.io/lego/dns/metaname/) |
|
| [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) | [Loopia](https://go-acme.github.io/lego/dns/loopia/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Manual](https://go-acme.github.io/lego/dns/manual/) |
|
||||||
| [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) |
|
| [Metaname](https://go-acme.github.io/lego/dns/metaname/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) |
|
||||||
| [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [NearlyFreeSpeech.NET](https://go-acme.github.io/lego/dns/nearlyfreespeech/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) |
|
| [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [NearlyFreeSpeech.NET](https://go-acme.github.io/lego/dns/nearlyfreespeech/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) |
|
||||||
| [Nicmanager](https://go-acme.github.io/lego/dns/nicmanager/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [Njalla](https://go-acme.github.io/lego/dns/njalla/) | [Nodion](https://go-acme.github.io/lego/dns/nodion/) |
|
| [Netlify](https://go-acme.github.io/lego/dns/netlify/) | [Nicmanager](https://go-acme.github.io/lego/dns/nicmanager/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [Njalla](https://go-acme.github.io/lego/dns/njalla/) |
|
||||||
| [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [OVH](https://go-acme.github.io/lego/dns/ovh/) |
|
| [Nodion](https://go-acme.github.io/lego/dns/nodion/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) |
|
||||||
| [plesk.com](https://go-acme.github.io/lego/dns/plesk/) | [Porkbun](https://go-acme.github.io/lego/dns/porkbun/) | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) |
|
| [OVH](https://go-acme.github.io/lego/dns/ovh/) | [plesk.com](https://go-acme.github.io/lego/dns/plesk/) | [Porkbun](https://go-acme.github.io/lego/dns/porkbun/) | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) |
|
||||||
| [RcodeZero](https://go-acme.github.io/lego/dns/rcodezero/) | [reg.ru](https://go-acme.github.io/lego/dns/regru/) | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) |
|
| [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [RcodeZero](https://go-acme.github.io/lego/dns/rcodezero/) | [reg.ru](https://go-acme.github.io/lego/dns/regru/) | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) |
|
||||||
| [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) |
|
| [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) |
|
||||||
| [Simply.com](https://go-acme.github.io/lego/dns/simply/) | [Sonic](https://go-acme.github.io/lego/dns/sonic/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/) |
|
| [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Simply.com](https://go-acme.github.io/lego/dns/simply/) | [Sonic](https://go-acme.github.io/lego/dns/sonic/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) |
|
||||||
| [TransIP](https://go-acme.github.io/lego/dns/transip/) | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/) | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/) | [Variomedia](https://go-acme.github.io/lego/dns/variomedia/) |
|
| [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/) | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/) |
|
||||||
| [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Vercel](https://go-acme.github.io/lego/dns/vercel/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) |
|
| [Variomedia](https://go-acme.github.io/lego/dns/variomedia/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Vercel](https://go-acme.github.io/lego/dns/vercel/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) |
|
||||||
| [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [Webnames](https://go-acme.github.io/lego/dns/webnames/) |
|
| [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) | [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) |
|
||||||
| [Websupport](https://go-acme.github.io/lego/dns/websupport/) | [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex 360](https://go-acme.github.io/lego/dns/yandex360/) | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) |
|
| [Webnames](https://go-acme.github.io/lego/dns/webnames/) | [Websupport](https://go-acme.github.io/lego/dns/websupport/) | [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex 360](https://go-acme.github.io/lego/dns/yandex360/) |
|
||||||
| [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | |
|
| [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) | [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) |
|
||||||
|
|
||||||
<!-- END DNS PROVIDERS LIST -->
|
<!-- END DNS PROVIDERS LIST -->
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ func allDNSCodes() string {
|
||||||
"cloudxns",
|
"cloudxns",
|
||||||
"conoha",
|
"conoha",
|
||||||
"constellix",
|
"constellix",
|
||||||
|
"cpanel",
|
||||||
"derak",
|
"derak",
|
||||||
"desec",
|
"desec",
|
||||||
"designate",
|
"designate",
|
||||||
|
@ -611,6 +612,31 @@ func displayDNSHelp(w io.Writer, name string) error {
|
||||||
ew.writeln()
|
ew.writeln()
|
||||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/constellix`)
|
ew.writeln(`More information: https://go-acme.github.io/lego/dns/constellix`)
|
||||||
|
|
||||||
|
case "cpanel":
|
||||||
|
// generated from: providers/dns/cpanel/cpanel.toml
|
||||||
|
ew.writeln(`Configuration for CPanel/WHM.`)
|
||||||
|
ew.writeln(`Code: 'cpanel'`)
|
||||||
|
ew.writeln(`Since: 'v4.16.0'`)
|
||||||
|
ew.writeln()
|
||||||
|
|
||||||
|
ew.writeln(`Credentials:`)
|
||||||
|
ew.writeln(` - "CPANEL_BASE_URL": API server URL`)
|
||||||
|
ew.writeln(` - "CPANEL_NAMESERVER": Nameserver`)
|
||||||
|
ew.writeln(` - "CPANEL_TOKEN": API token`)
|
||||||
|
ew.writeln(` - "CPANEL_USERNAME": username`)
|
||||||
|
ew.writeln()
|
||||||
|
|
||||||
|
ew.writeln(`Additional Configuration:`)
|
||||||
|
ew.writeln(` - "CPANEL_HTTP_TIMEOUT": API request timeout`)
|
||||||
|
ew.writeln(` - "CPANEL_MODE": use cpanel API or WHM API (Default: cpanel)`)
|
||||||
|
ew.writeln(` - "CPANEL_POLLING_INTERVAL": Time between DNS propagation check`)
|
||||||
|
ew.writeln(` - "CPANEL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
|
||||||
|
ew.writeln(` - "CPANEL_REGION": The region`)
|
||||||
|
ew.writeln(` - "CPANEL_TTL": The TTL of the TXT record used for the DNS challenge`)
|
||||||
|
|
||||||
|
ew.writeln()
|
||||||
|
ew.writeln(`More information: https://go-acme.github.io/lego/dns/cpanel`)
|
||||||
|
|
||||||
case "derak":
|
case "derak":
|
||||||
// generated from: providers/dns/derak/derak.toml
|
// generated from: providers/dns/derak/derak.toml
|
||||||
ew.writeln(`Configuration for Derak Cloud.`)
|
ew.writeln(`Configuration for Derak Cloud.`)
|
||||||
|
|
86
docs/content/dns/zz_gen_cpanel.md
Normal file
86
docs/content/dns/zz_gen_cpanel.md
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
---
|
||||||
|
title: "CPanel/WHM"
|
||||||
|
date: 2019-03-03T16:39:46+01:00
|
||||||
|
draft: false
|
||||||
|
slug: cpanel
|
||||||
|
dnsprovider:
|
||||||
|
since: "v4.16.0"
|
||||||
|
code: "cpanel"
|
||||||
|
url: "https://cpanel.net/"
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||||
|
<!-- providers/dns/cpanel/cpanel.toml -->
|
||||||
|
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||||
|
|
||||||
|
|
||||||
|
Configuration for [CPanel/WHM](https://cpanel.net/).
|
||||||
|
|
||||||
|
|
||||||
|
<!--more-->
|
||||||
|
|
||||||
|
- Code: `cpanel`
|
||||||
|
- Since: v4.16.0
|
||||||
|
|
||||||
|
|
||||||
|
Here is an example bash command using the CPanel/WHM provider:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
### CPANEL (default)
|
||||||
|
|
||||||
|
CPANEL_USERNAME = "yyyy"
|
||||||
|
CPANEL_TOKEN = "xxxx"
|
||||||
|
CPANEL_BASE_URL = "https://example.com:2083" \
|
||||||
|
CPANEL_NAMESERVER = "ns1.example.com:53" \
|
||||||
|
lego --email you@example.com --dns cpanel --domains my.example.org run
|
||||||
|
|
||||||
|
## WHM
|
||||||
|
|
||||||
|
CPANEL_MODE = whm
|
||||||
|
CPANEL_USERNAME = "yyyy"
|
||||||
|
CPANEL_TOKEN = "xxxx"
|
||||||
|
CPANEL_BASE_URL = "https://example.com:2087" \
|
||||||
|
CPANEL_NAMESERVER = "ns1.example.com:53" \
|
||||||
|
lego --email you@example.com --dns cpanel --domains my.example.org run
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
|
||||||
|
| Environment Variable Name | Description |
|
||||||
|
|-----------------------|-------------|
|
||||||
|
| `CPANEL_BASE_URL` | API server URL |
|
||||||
|
| `CPANEL_NAMESERVER` | Nameserver |
|
||||||
|
| `CPANEL_TOKEN` | API token |
|
||||||
|
| `CPANEL_USERNAME` | username |
|
||||||
|
|
||||||
|
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||||
|
More information [here]({{< ref "dns#configuration-and-credentials" >}}).
|
||||||
|
|
||||||
|
|
||||||
|
## Additional Configuration
|
||||||
|
|
||||||
|
| Environment Variable Name | Description |
|
||||||
|
|--------------------------------|-------------|
|
||||||
|
| `CPANEL_HTTP_TIMEOUT` | API request timeout |
|
||||||
|
| `CPANEL_MODE` | use cpanel API or WHM API (Default: cpanel) |
|
||||||
|
| `CPANEL_POLLING_INTERVAL` | Time between DNS propagation check |
|
||||||
|
| `CPANEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
|
||||||
|
| `CPANEL_REGION` | The region |
|
||||||
|
| `CPANEL_TTL` | The TTL of the TXT record used for the DNS challenge |
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||||
|
<!-- providers/dns/cpanel/cpanel.toml -->
|
||||||
|
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
|
@ -137,7 +137,7 @@ To display the documentation for a specific DNS provider, run:
|
||||||
$ lego dnshelp -c code
|
$ lego dnshelp -c code
|
||||||
|
|
||||||
Supported DNS providers:
|
Supported DNS providers:
|
||||||
acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, azuredns, bindman, bluecat, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, constellix, derak, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, efficientip, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpnet, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, manual, metaname, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rcodezero, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, servercow, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, webnames, websupport, wedos, yandex, yandex360, yandexcloud, zoneee, zonomi
|
acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, azuredns, bindman, bluecat, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, constellix, cpanel, derak, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, efficientip, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpnet, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, manual, metaname, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rcodezero, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, servercow, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, webnames, websupport, wedos, yandex, yandex360, yandexcloud, zoneee, zonomi
|
||||||
|
|
||||||
More information: https://go-acme.github.io/lego/dns
|
More information: https://go-acme.github.io/lego/dns
|
||||||
"""
|
"""
|
||||||
|
|
346
providers/dns/cpanel/cpanel.go
Normal file
346
providers/dns/cpanel/cpanel.go
Normal file
|
@ -0,0 +1,346 @@
|
||||||
|
// Package cpanel implements a DNS provider for solving the DNS-01 challenge using CPanel.
|
||||||
|
package cpanel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"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/cpanel/internal/cpanel"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/whm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Environment variables names.
|
||||||
|
const (
|
||||||
|
envNamespace = "CPANEL_"
|
||||||
|
|
||||||
|
EnvMode = envNamespace + "MODE"
|
||||||
|
EnvUsername = envNamespace + "USERNAME"
|
||||||
|
EnvToken = envNamespace + "TOKEN"
|
||||||
|
EnvBaseURL = envNamespace + "BASE_URL"
|
||||||
|
EnvNameserver = envNamespace + "NAMESERVER"
|
||||||
|
|
||||||
|
EnvTTL = envNamespace + "TTL"
|
||||||
|
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||||
|
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||||
|
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiClient interface {
|
||||||
|
FetchZoneInformation(ctx context.Context, domain string) ([]shared.ZoneRecord, error)
|
||||||
|
AddRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error)
|
||||||
|
EditRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error)
|
||||||
|
DeleteRecord(ctx context.Context, serial uint32, domain string, lineIndex int) (*shared.ZoneSerial, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is used to configure the creation of the DNSProvider.
|
||||||
|
type Config struct {
|
||||||
|
Mode string
|
||||||
|
Username string
|
||||||
|
Token string
|
||||||
|
BaseURL string
|
||||||
|
Nameserver string
|
||||||
|
TTL int
|
||||||
|
PropagationTimeout time.Duration
|
||||||
|
PollingInterval time.Duration
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||||
|
func NewDefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Mode: env.GetOrDefaultString(EnvMode, "cpanel"),
|
||||||
|
TTL: env.GetOrDefaultInt(EnvTTL, 300),
|
||||||
|
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 apiClient
|
||||||
|
dnsClient *shared.DNSClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProvider returns a DNSProvider instance configured for CPanel.
|
||||||
|
// Credentials must be passed in the environment variables:
|
||||||
|
// CPANEL_USERNAME, CPANEL_TOKEN, CPANEL_BASE_URL, CPANEL_NAMESERVER.
|
||||||
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
values, err := env.Get(EnvUsername, EnvToken, EnvBaseURL, EnvNameserver)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cpanel: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := NewDefaultConfig()
|
||||||
|
config.Username = values[EnvUsername]
|
||||||
|
config.Token = values[EnvToken]
|
||||||
|
config.BaseURL = values[EnvBaseURL]
|
||||||
|
config.Nameserver = values[EnvNameserver]
|
||||||
|
|
||||||
|
return NewDNSProviderConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderConfig return a DNSProvider instance configured for CPanel.
|
||||||
|
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("cpanel: the configuration of the DNS provider is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Username == "" || config.Token == "" {
|
||||||
|
return nil, errors.New("cpanel: some credentials information are missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.BaseURL == "" || config.Nameserver == "" {
|
||||||
|
return nil, errors.New("cpanel: server information are missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := createClient(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cpanel: create client error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSProvider{
|
||||||
|
config: config,
|
||||||
|
client: client,
|
||||||
|
dnsClient: shared.NewDNSClient(10 * time.Second),
|
||||||
|
}, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record to fulfill the dns-01 challenge.
|
||||||
|
func (d *DNSProvider) Present(domain, _, keyAuth string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||||
|
|
||||||
|
effectiveDomain := strings.TrimPrefix(info.EffectiveFQDN, "_acme-challenge.")
|
||||||
|
|
||||||
|
soa, err := d.dnsClient.SOACall(effectiveDomain, d.config.Nameserver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cpanel[mode=%s]: could not find SOA for domain %q (%s) in %s: %w", d.config.Mode, domain, info.EffectiveFQDN, d.config.Nameserver, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zone := dns01.UnFqdn(soa.Hdr.Name)
|
||||||
|
|
||||||
|
zoneInfo, err := d.client.FetchZoneInformation(ctx, zone)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cpanel[mode=%s]: fetch zone information: %w", d.config.Mode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serial, err := getZoneSerial(soa.Hdr.Name, zoneInfo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cpanel[mode=%s]: get zone serial: %w", d.config.Mode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
valueB64 := base64.StdEncoding.EncodeToString([]byte(info.Value))
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
var existingRecord shared.ZoneRecord
|
||||||
|
for _, record := range zoneInfo {
|
||||||
|
if contains(record.DataB64, valueB64) {
|
||||||
|
existingRecord = record
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record := shared.Record{
|
||||||
|
DName: info.EffectiveFQDN,
|
||||||
|
TTL: d.config.TTL,
|
||||||
|
RecordType: "TXT",
|
||||||
|
}
|
||||||
|
|
||||||
|
// New record.
|
||||||
|
if !found {
|
||||||
|
record.Data = []string{info.Value}
|
||||||
|
|
||||||
|
_, err = d.client.AddRecord(ctx, serial, zone, record)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cpanel[mode=%s]: add record: %w", d.config.Mode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing record.
|
||||||
|
record.LineIndex = existingRecord.LineIndex
|
||||||
|
|
||||||
|
for _, dataB64 := range existingRecord.DataB64 {
|
||||||
|
data, errD := base64.StdEncoding.DecodeString(dataB64)
|
||||||
|
if errD != nil {
|
||||||
|
return fmt.Errorf("cpanel[mode=%s]: decode base64 record value: %w", d.config.Mode, errD)
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Data = append(record.Data, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Data = append(record.Data, info.Value)
|
||||||
|
|
||||||
|
_, err = d.client.EditRecord(ctx, serial, zone, record)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cpanel[mode=%s]: edit record: %w", d.config.Mode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp removes the TXT record matching the specified parameters.
|
||||||
|
func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||||
|
|
||||||
|
soa, err := d.dnsClient.SOACall(strings.TrimPrefix(info.EffectiveFQDN, "_acme-challenge."), d.config.Nameserver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cpanel[mode=%s]: could not find SOA for domain %q (%s) in %s: %w", d.config.Mode, domain, info.EffectiveFQDN, d.config.Nameserver, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zone := dns01.UnFqdn(soa.Hdr.Name)
|
||||||
|
|
||||||
|
zoneInfo, err := d.client.FetchZoneInformation(ctx, zone)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cpanel[mode=%s]: fetch zone information: %w", d.config.Mode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serial, err := getZoneSerial(soa.Hdr.Name, zoneInfo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cpanel[mode=%s]: get zone serial: %w", d.config.Mode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
valueB64 := base64.StdEncoding.EncodeToString([]byte(info.Value))
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
var existingRecord shared.ZoneRecord
|
||||||
|
for _, record := range zoneInfo {
|
||||||
|
if contains(record.DataB64, valueB64) {
|
||||||
|
existingRecord = record
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var newData []string
|
||||||
|
for _, dataB64 := range existingRecord.DataB64 {
|
||||||
|
if dataB64 == valueB64 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data, errD := base64.StdEncoding.DecodeString(dataB64)
|
||||||
|
if errD != nil {
|
||||||
|
return fmt.Errorf("cpanel[mode=%s]: decode base64 record value: %w", d.config.Mode, errD)
|
||||||
|
}
|
||||||
|
|
||||||
|
newData = append(newData, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete record.
|
||||||
|
if len(newData) == 0 {
|
||||||
|
_, err = d.client.DeleteRecord(ctx, serial, zone, existingRecord.LineIndex)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cpanel[mode=%s]: delete record: %w", d.config.Mode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove one value.
|
||||||
|
record := shared.Record{
|
||||||
|
DName: info.EffectiveFQDN,
|
||||||
|
TTL: d.config.TTL,
|
||||||
|
RecordType: "TXT",
|
||||||
|
Data: newData,
|
||||||
|
LineIndex: existingRecord.LineIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.client.EditRecord(ctx, serial, zone, record)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cpanel[mode=%s]: edit record: %w", d.config.Mode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getZoneSerial(zoneFqdn string, zoneInfo []shared.ZoneRecord) (uint32, error) {
|
||||||
|
nameB64 := base64.StdEncoding.EncodeToString([]byte(zoneFqdn))
|
||||||
|
|
||||||
|
for _, record := range zoneInfo {
|
||||||
|
if record.Type != "record" || record.RecordType != "SOA" || record.DNameB64 != nameB64 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/go-acme/lego/issues/1060#issuecomment-1925572386
|
||||||
|
// https://github.com/go-acme/lego/issues/1060#issuecomment-1925581832
|
||||||
|
data, err := base64.StdEncoding.DecodeString(record.DataB64[2])
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("decode serial DNameB64: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var newSerial uint32
|
||||||
|
_, err = fmt.Sscan(string(data), &newSerial)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("decode serial DNameB64, invalid serial value %q: %w", string(data), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSerial, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, errors.New("zone serial not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func createClient(config *Config) (apiClient, error) {
|
||||||
|
switch strings.ToLower(config.Mode) {
|
||||||
|
case "cpanel":
|
||||||
|
client, err := cpanel.NewClient(config.BaseURL, config.Username, config.Token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create cPanel API client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.HTTPClient != nil {
|
||||||
|
client.HTTPClient = config.HTTPClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
|
||||||
|
case "whm":
|
||||||
|
client, err := whm.NewClient(config.BaseURL, config.Username, config.Token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create WHM API client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.HTTPClient != nil {
|
||||||
|
client.HTTPClient = config.HTTPClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported mode: %q", config.Mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(values []string, value string) bool {
|
||||||
|
for _, v := range values {
|
||||||
|
if v == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
42
providers/dns/cpanel/cpanel.toml
Normal file
42
providers/dns/cpanel/cpanel.toml
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
Name = "CPanel/WHM"
|
||||||
|
Description = ''''''
|
||||||
|
URL = "https://cpanel.net/"
|
||||||
|
Code = "cpanel"
|
||||||
|
Since = "v4.16.0"
|
||||||
|
|
||||||
|
Example = '''
|
||||||
|
### CPANEL (default)
|
||||||
|
|
||||||
|
CPANEL_USERNAME = "yyyy"
|
||||||
|
CPANEL_TOKEN = "xxxx"
|
||||||
|
CPANEL_BASE_URL = "https://example.com:2083" \
|
||||||
|
CPANEL_NAMESERVER = "ns1.example.com:53" \
|
||||||
|
lego --email you@example.com --dns cpanel --domains my.example.org run
|
||||||
|
|
||||||
|
## WHM
|
||||||
|
|
||||||
|
CPANEL_MODE = whm
|
||||||
|
CPANEL_USERNAME = "yyyy"
|
||||||
|
CPANEL_TOKEN = "xxxx"
|
||||||
|
CPANEL_BASE_URL = "https://example.com:2087" \
|
||||||
|
CPANEL_NAMESERVER = "ns1.example.com:53" \
|
||||||
|
lego --email you@example.com --dns cpanel --domains my.example.org run
|
||||||
|
'''
|
||||||
|
|
||||||
|
[Configuration]
|
||||||
|
[Configuration.Credentials]
|
||||||
|
CPANEL_USERNAME = "username"
|
||||||
|
CPANEL_TOKEN = "API token"
|
||||||
|
CPANEL_BASE_URL = "API server URL"
|
||||||
|
CPANEL_NAMESERVER = "Nameserver"
|
||||||
|
[Configuration.Additional]
|
||||||
|
CPANEL_MODE = "use cpanel API or WHM API (Default: cpanel)"
|
||||||
|
CPANEL_POLLING_INTERVAL = "Time between DNS propagation check"
|
||||||
|
CPANEL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
|
||||||
|
CPANEL_TTL = "The TTL of the TXT record used for the DNS challenge"
|
||||||
|
CPANEL_HTTP_TIMEOUT = "API request timeout"
|
||||||
|
CPANEL_REGION = "The region"
|
||||||
|
|
||||||
|
[Links]
|
||||||
|
API_CPANEL = "https://api.docs.cpanel.net/cpanel/introduction/"
|
||||||
|
API_WHM = "https://api.docs.cpanel.net/whm/introduction/"
|
338
providers/dns/cpanel/cpanel_test.go
Normal file
338
providers/dns/cpanel/cpanel_test.go
Normal file
|
@ -0,0 +1,338 @@
|
||||||
|
package cpanel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/platform/tester"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const envDomain = envNamespace + "DOMAIN"
|
||||||
|
|
||||||
|
var envTest = tester.NewEnvTest(
|
||||||
|
EnvMode,
|
||||||
|
EnvUsername,
|
||||||
|
EnvToken,
|
||||||
|
EnvBaseURL,
|
||||||
|
EnvNameserver).
|
||||||
|
WithDomain(envDomain)
|
||||||
|
|
||||||
|
func TestNewDNSProvider(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
envVars map[string]string
|
||||||
|
expected string
|
||||||
|
expectedMode string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "success cpanel mode (default)",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvUsername: "user",
|
||||||
|
EnvToken: "secret",
|
||||||
|
EnvBaseURL: "https://example.com",
|
||||||
|
EnvNameserver: "ns.example.com:53",
|
||||||
|
},
|
||||||
|
expectedMode: "cpanel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "success whm mode",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvMode: "whm",
|
||||||
|
EnvUsername: "user",
|
||||||
|
EnvToken: "secret",
|
||||||
|
EnvBaseURL: "https://example.com",
|
||||||
|
EnvNameserver: "ns.example.com:53",
|
||||||
|
},
|
||||||
|
expectedMode: "whm",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing user",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvToken: "secret",
|
||||||
|
EnvBaseURL: "https://example.com",
|
||||||
|
EnvNameserver: "ns.example.com:53",
|
||||||
|
},
|
||||||
|
expected: "cpanel: some credentials information are missing: CPANEL_USERNAME",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing token",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvUsername: "user",
|
||||||
|
EnvBaseURL: "https://example.com",
|
||||||
|
EnvNameserver: "ns.example.com:53",
|
||||||
|
},
|
||||||
|
expected: "cpanel: some credentials information are missing: CPANEL_TOKEN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing base URL",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvUsername: "user",
|
||||||
|
EnvToken: "secret",
|
||||||
|
EnvBaseURL: "",
|
||||||
|
EnvNameserver: "ns.example.com:53",
|
||||||
|
},
|
||||||
|
expected: "cpanel: some credentials information are missing: CPANEL_BASE_URL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing nameserver",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvUsername: "user",
|
||||||
|
EnvToken: "secret",
|
||||||
|
EnvBaseURL: "https://example.com",
|
||||||
|
EnvNameserver: "",
|
||||||
|
},
|
||||||
|
expected: "cpanel: some credentials information are missing: CPANEL_NAMESERVER",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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 == "" {
|
||||||
|
assert.Equal(t, test.expectedMode, p.config.Mode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, p)
|
||||||
|
require.NotNil(t, p.config)
|
||||||
|
} else {
|
||||||
|
require.EqualError(t, err, test.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderConfig(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
mode string
|
||||||
|
username string
|
||||||
|
token string
|
||||||
|
baseURL string
|
||||||
|
nameserver string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "success",
|
||||||
|
mode: "whm",
|
||||||
|
username: "user",
|
||||||
|
token: "secret",
|
||||||
|
baseURL: "https://example.com",
|
||||||
|
nameserver: "ns.example.com:53",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing mode",
|
||||||
|
username: "user",
|
||||||
|
token: "secret",
|
||||||
|
baseURL: "https://example.com",
|
||||||
|
nameserver: "ns.example.com:53",
|
||||||
|
expected: `cpanel: create client error: unsupported mode: ""`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "invalid mode",
|
||||||
|
mode: "test",
|
||||||
|
username: "user",
|
||||||
|
token: "secret",
|
||||||
|
baseURL: "https://example.com",
|
||||||
|
nameserver: "ns.example.com:53",
|
||||||
|
expected: `cpanel: create client error: unsupported mode: "test"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing username",
|
||||||
|
mode: "whm",
|
||||||
|
username: "",
|
||||||
|
token: "secret",
|
||||||
|
baseURL: "https://example.com",
|
||||||
|
nameserver: "ns.example.com:53",
|
||||||
|
expected: "cpanel: some credentials information are missing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing token",
|
||||||
|
mode: "whm",
|
||||||
|
username: "user",
|
||||||
|
token: "",
|
||||||
|
baseURL: "https://example.com",
|
||||||
|
nameserver: "ns.example.com:53",
|
||||||
|
expected: "cpanel: some credentials information are missing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing base URL",
|
||||||
|
mode: "whm",
|
||||||
|
username: "user",
|
||||||
|
token: "secret",
|
||||||
|
baseURL: "",
|
||||||
|
nameserver: "ns.example.com:53",
|
||||||
|
expected: "cpanel: server information are missing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing nameserver",
|
||||||
|
mode: "whm",
|
||||||
|
username: "user",
|
||||||
|
token: "secret",
|
||||||
|
baseURL: "https://example.com",
|
||||||
|
nameserver: "",
|
||||||
|
expected: "cpanel: server information are missing",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
config := NewDefaultConfig()
|
||||||
|
config.Mode = test.mode
|
||||||
|
config.Username = test.username
|
||||||
|
config.Token = test.token
|
||||||
|
config.BaseURL = test.baseURL
|
||||||
|
config.Nameserver = test.nameserver
|
||||||
|
|
||||||
|
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 Test_getZoneSerial(t *testing.T) {
|
||||||
|
zones := []shared.ZoneRecord{
|
||||||
|
{
|
||||||
|
Type: "comment",
|
||||||
|
LineIndex: 1,
|
||||||
|
TextB64: "OyBab25lIGZpbGUgZm9yIGV4YW1wbGUuY29t",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "control",
|
||||||
|
LineIndex: 2,
|
||||||
|
TextB64: "JFRUTCAxNDQwMA==",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DNameB64: "ZXhhbXBsZS5jb20u",
|
||||||
|
LineIndex: 4,
|
||||||
|
RecordType: "NS",
|
||||||
|
Type: "record",
|
||||||
|
TTL: 86400,
|
||||||
|
DataB64: []string{"YWxsMS5kbnNyb3VuZHJvYmluLm5ldC4="},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DataB64: []string{
|
||||||
|
"YWxsMS5kbnNyb3VuZHJvYmluLm5ldC4=",
|
||||||
|
"ZW1haWwuaXB4Y29yZS5jb20u",
|
||||||
|
"MjAyNDAyMDQwOQ==",
|
||||||
|
"MzYwMA==",
|
||||||
|
"MTgwMA==",
|
||||||
|
"MTIwOTYwMA==",
|
||||||
|
"ODY0MDA=",
|
||||||
|
},
|
||||||
|
RecordType: "SOA",
|
||||||
|
Type: "record",
|
||||||
|
TTL: 86400,
|
||||||
|
LineIndex: 3,
|
||||||
|
DNameB64: "ZXhhbXBsZS5jb20u",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RecordType: "A",
|
||||||
|
Type: "record",
|
||||||
|
TTL: 3600,
|
||||||
|
DataB64: []string{"MTAuMTAuMTAuMTA="},
|
||||||
|
LineIndex: 9,
|
||||||
|
DNameB64: "ZXhhbXBsZS5jb20u",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
serial, err := getZoneSerial("example.com.", zones)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 2024020409, serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getZoneSerial_error(t *testing.T) {
|
||||||
|
zones := []shared.ZoneRecord{
|
||||||
|
{
|
||||||
|
Type: "comment",
|
||||||
|
LineIndex: 1,
|
||||||
|
TextB64: "OyBab25lIGZpbGUgZm9yIGV4YW1wbGUuY29t",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "control",
|
||||||
|
LineIndex: 2,
|
||||||
|
TextB64: "JFRUTCAxNDQwMA==",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DNameB64: "ZXhhbXBsZS5jb20u",
|
||||||
|
LineIndex: 4,
|
||||||
|
RecordType: "NS",
|
||||||
|
Type: "record",
|
||||||
|
TTL: 86400,
|
||||||
|
DataB64: []string{"YWxsMS5kbnNyb3VuZHJvYmluLm5ldC4="},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DataB64: []string{
|
||||||
|
"YWxsMS5kbnNyb3VuZHJvYmluLm5ldC4=",
|
||||||
|
"ZW1haWwuaXB4Y29yZS5jb20u",
|
||||||
|
"MjAyNDAyMDQwOQ==",
|
||||||
|
"MzYwMA==",
|
||||||
|
"MTgwMA==",
|
||||||
|
"MTIwOTYwMA==",
|
||||||
|
"ODY0MDA=",
|
||||||
|
},
|
||||||
|
RecordType: "SOA",
|
||||||
|
Type: "record",
|
||||||
|
TTL: 86400,
|
||||||
|
LineIndex: 3,
|
||||||
|
DNameB64: "ZXhhbXBsZS5vcmcu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RecordType: "A",
|
||||||
|
Type: "record",
|
||||||
|
TTL: 3600,
|
||||||
|
DataB64: []string{"MTAuMTAuMTAuMTA="},
|
||||||
|
LineIndex: 9,
|
||||||
|
DNameB64: "ZXhhbXBsZS5jb20u",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
serial, err := getZoneSerial("example.com.", zones)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 0, serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
155
providers/dns/cpanel/internal/cpanel/client.go
Normal file
155
providers/dns/cpanel/internal/cpanel/client.go
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
package cpanel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const statusFailed = 0
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
username string
|
||||||
|
token string
|
||||||
|
|
||||||
|
baseURL *url.URL
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(baseURL string, username string, token string) (*Client, error) {
|
||||||
|
apiEndpoint, err := url.Parse(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
username: username,
|
||||||
|
token: token,
|
||||||
|
baseURL: apiEndpoint.JoinPath("execute"),
|
||||||
|
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchZoneInformation fetches zone information.
|
||||||
|
// https://api.docs.cpanel.net/openapi/cpanel/operation/dns-parse_zone/
|
||||||
|
func (c Client) FetchZoneInformation(ctx context.Context, domain string) ([]shared.ZoneRecord, error) {
|
||||||
|
endpoint := c.baseURL.JoinPath("DNS", "parse_zone")
|
||||||
|
|
||||||
|
query := endpoint.Query()
|
||||||
|
query.Set("zone", domain)
|
||||||
|
endpoint.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
var result APIResponse[[]shared.ZoneRecord]
|
||||||
|
|
||||||
|
err := c.doRequest(ctx, endpoint, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Status == statusFailed {
|
||||||
|
return nil, toError(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRecord adds a new record.
|
||||||
|
//
|
||||||
|
// add='{"dname":"example", "ttl":14400, "record_type":"TXT", "data":["string1", "string2"]}'
|
||||||
|
func (c Client) AddRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) {
|
||||||
|
data, err := json.Marshal(record)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request JSON data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.updateZone(ctx, serial, domain, "add", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditRecord edits an existing record.
|
||||||
|
//
|
||||||
|
// edit='{"line_index": 9, "dname":"example", "ttl":14400, "record_type":"TXT", "data":["string1", "string2"]}'
|
||||||
|
func (c Client) EditRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) {
|
||||||
|
data, err := json.Marshal(record)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request JSON data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.updateZone(ctx, serial, domain, "edit", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRecord deletes an existing record.
|
||||||
|
//
|
||||||
|
// remove=22
|
||||||
|
func (c Client) DeleteRecord(ctx context.Context, serial uint32, domain string, lineIndex int) (*shared.ZoneSerial, error) {
|
||||||
|
return c.updateZone(ctx, serial, domain, "remove", strconv.Itoa(lineIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://api.docs.cpanel.net/openapi/cpanel/operation/dns-mass_edit_zone/
|
||||||
|
func (c Client) updateZone(ctx context.Context, serial uint32, domain, action, data string) (*shared.ZoneSerial, error) {
|
||||||
|
endpoint := c.baseURL.JoinPath("DNS", "mass_edit_zone")
|
||||||
|
|
||||||
|
query := endpoint.Query()
|
||||||
|
query.Set("serial", strconv.FormatUint(uint64(serial), 10))
|
||||||
|
query.Set(action, data)
|
||||||
|
query.Set("zone", domain)
|
||||||
|
endpoint.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
var result APIResponse[shared.ZoneSerial]
|
||||||
|
|
||||||
|
err := c.doRequest(ctx, endpoint, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Status == statusFailed {
|
||||||
|
return nil, toError(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) doRequest(ctx context.Context, endpoint *url.URL, result any) error {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://api.docs.cpanel.net/cpanel/tokens/#using-an-api-token
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("cpanel %s:%s", c.username, c.token))
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return errutils.NewHTTPDoError(req, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return errutils.NewUnexpectedResponseStatusCodeError(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
|
||||||
|
}
|
170
providers/dns/cpanel/internal/cpanel/client_test.go
Normal file
170
providers/dns/cpanel/internal/cpanel/client_test.go
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
package cpanel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTest(t *testing.T, pattern string, filename string) *Client {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
open, err := os.Open(filepath.Join("fixtures", filename))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = open.Close() }()
|
||||||
|
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
_, err = io.Copy(rw, open)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
client, err := NewClient(server.URL, "user", "secret")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client.HTTPClient = server.Client()
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_FetchZoneInformation(t *testing.T) {
|
||||||
|
client := setupTest(t, "/execute/DNS/parse_zone", "zone-info.json")
|
||||||
|
|
||||||
|
zoneInfo, err := client.FetchZoneInformation(context.Background(), "example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := []shared.ZoneRecord{{
|
||||||
|
LineIndex: 22,
|
||||||
|
Type: "record",
|
||||||
|
DataB64: []string{"dGV4YXMuY29tLg=="},
|
||||||
|
DNameB64: "dGV4YXMuY29tLg==",
|
||||||
|
RecordType: "MX",
|
||||||
|
TTL: 14400,
|
||||||
|
}}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, zoneInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_FetchZoneInformation_error(t *testing.T) {
|
||||||
|
client := setupTest(t, "/execute/DNS/parse_zone", "zone-info_error.json")
|
||||||
|
|
||||||
|
zoneInfo, err := client.FetchZoneInformation(context.Background(), "example.com")
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, zoneInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AddRecord(t *testing.T) {
|
||||||
|
client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone.json")
|
||||||
|
|
||||||
|
record := shared.Record{
|
||||||
|
DName: "example",
|
||||||
|
TTL: 14400,
|
||||||
|
RecordType: "TXT",
|
||||||
|
Data: []string{"string1", "string2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneSerial, err := client.AddRecord(context.Background(), 123456, "example.com", record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &shared.ZoneSerial{NewSerial: "2021031903"}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, zoneSerial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AddRecord_error(t *testing.T) {
|
||||||
|
client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone_error.json")
|
||||||
|
|
||||||
|
record := shared.Record{
|
||||||
|
DName: "example",
|
||||||
|
TTL: 14400,
|
||||||
|
RecordType: "TXT",
|
||||||
|
Data: []string{"string1", "string2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneSerial, err := client.AddRecord(context.Background(), 123456, "example.com", record)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, zoneSerial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_EditRecord(t *testing.T) {
|
||||||
|
client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone.json")
|
||||||
|
|
||||||
|
record := shared.Record{
|
||||||
|
LineIndex: 9,
|
||||||
|
DName: "example",
|
||||||
|
TTL: 14400,
|
||||||
|
RecordType: "TXT",
|
||||||
|
Data: []string{"string1", "string2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneSerial, err := client.EditRecord(context.Background(), 123456, "example.com", record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &shared.ZoneSerial{NewSerial: "2021031903"}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, zoneSerial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_EditRecord_error(t *testing.T) {
|
||||||
|
client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone_error.json")
|
||||||
|
|
||||||
|
record := shared.Record{
|
||||||
|
LineIndex: 9,
|
||||||
|
DName: "example",
|
||||||
|
TTL: 14400,
|
||||||
|
RecordType: "TXT",
|
||||||
|
Data: []string{"string1", "string2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneSerial, err := client.EditRecord(context.Background(), 123456, "example.com", record)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, zoneSerial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_DeleteRecord(t *testing.T) {
|
||||||
|
client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone.json")
|
||||||
|
|
||||||
|
zoneSerial, err := client.DeleteRecord(context.Background(), 123456, "example.com", 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &shared.ZoneSerial{NewSerial: "2021031903"}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, zoneSerial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_DeleteRecord_error(t *testing.T) {
|
||||||
|
client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone_error.json")
|
||||||
|
|
||||||
|
zoneSerial, err := client.DeleteRecord(context.Background(), 123456, "example.com", 0)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, zoneSerial)
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"transformed": 1
|
||||||
|
},
|
||||||
|
"messages": null,
|
||||||
|
"status": 1,
|
||||||
|
"warnings": null,
|
||||||
|
"errors": null,
|
||||||
|
"data": {
|
||||||
|
"new_serial": "2021031903"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"warnings": null,
|
||||||
|
"messages": [
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
"c"
|
||||||
|
],
|
||||||
|
"data": null,
|
||||||
|
"errors": [
|
||||||
|
"You do not control a DNS zone named example.com."
|
||||||
|
],
|
||||||
|
"metadata": {},
|
||||||
|
"status": 0
|
||||||
|
}
|
21
providers/dns/cpanel/internal/cpanel/fixtures/zone-info.json
Normal file
21
providers/dns/cpanel/internal/cpanel/fixtures/zone-info.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"transformed": 1
|
||||||
|
},
|
||||||
|
"messages": null,
|
||||||
|
"status": 1,
|
||||||
|
"warnings": null,
|
||||||
|
"errors": null,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"line_index": 22,
|
||||||
|
"dname_b64": "dGV4YXMuY29tLg==",
|
||||||
|
"data_b64": [
|
||||||
|
"dGV4YXMuY29tLg=="
|
||||||
|
],
|
||||||
|
"type": "record",
|
||||||
|
"ttl": 14400,
|
||||||
|
"record_type": "MX"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"warnings": null,
|
||||||
|
"messages": [
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
"c"
|
||||||
|
],
|
||||||
|
"data": null,
|
||||||
|
"errors": [
|
||||||
|
"You do not control a DNS zone named example.com."
|
||||||
|
],
|
||||||
|
"metadata": {},
|
||||||
|
"status": 0
|
||||||
|
}
|
24
providers/dns/cpanel/internal/cpanel/types.go
Normal file
24
providers/dns/cpanel/internal/cpanel/types.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package cpanel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIResponse[T any] struct {
|
||||||
|
Metadata Metadata `json:"metadata,omitempty"`
|
||||||
|
Data T `json:"data,omitempty"`
|
||||||
|
|
||||||
|
Status int `json:"status,omitempty"`
|
||||||
|
Messages []string `json:"messages,omitempty"`
|
||||||
|
Warnings []string `json:"warnings,omitempty"`
|
||||||
|
Errors []string `json:"errors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
Transformed int `json:"transformed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toError[T any](r APIResponse[T]) error {
|
||||||
|
return fmt.Errorf("error(%d): %s: %s", r.Status, strings.Join(r.Errors, ", "), strings.Join(r.Messages, ", "))
|
||||||
|
}
|
67
providers/dns/cpanel/internal/shared/dns.go
Normal file
67
providers/dns/cpanel/internal/shared/dns.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DNSClient struct {
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDNSClient(timeout time.Duration) *DNSClient {
|
||||||
|
return &DNSClient{timeout: timeout}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DNSClient) SOACall(fqdn, nameserver string) (*dns.SOA, error) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(fqdn, dns.TypeSOA)
|
||||||
|
m.SetEdns0(4096, false)
|
||||||
|
|
||||||
|
in, err := d.sendDNSQuery(m, nameserver)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(in.Answer) == 0 {
|
||||||
|
if len(in.Ns) > 0 {
|
||||||
|
if soa, ok := in.Ns[0].(*dns.SOA); ok && fqdn != soa.Hdr.Name {
|
||||||
|
return d.SOACall(soa.Hdr.Name, nameserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("empty answer for %s in %s", fqdn, nameserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rr := range in.Answer {
|
||||||
|
if soa, ok := rr.(*dns.SOA); ok {
|
||||||
|
return soa, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("SOA not found for %s in %s", fqdn, nameserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DNSClient) sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {
|
||||||
|
if ok, _ := strconv.ParseBool(os.Getenv("LEGO_EXPERIMENTAL_DNS_TCP_ONLY")); ok {
|
||||||
|
tcp := &dns.Client{Net: "tcp", Timeout: d.timeout}
|
||||||
|
in, _, err := tcp.Exchange(m, ns)
|
||||||
|
|
||||||
|
return in, err
|
||||||
|
}
|
||||||
|
|
||||||
|
udp := &dns.Client{Net: "udp", Timeout: d.timeout}
|
||||||
|
in, _, err := udp.Exchange(m, ns)
|
||||||
|
|
||||||
|
if in != nil && in.Truncated {
|
||||||
|
tcp := &dns.Client{Net: "tcp", Timeout: d.timeout}
|
||||||
|
// If the TCP request succeeds, the err will reset to nil
|
||||||
|
in, _, err = tcp.Exchange(m, ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
return in, err
|
||||||
|
}
|
23
providers/dns/cpanel/internal/shared/types.go
Normal file
23
providers/dns/cpanel/internal/shared/types.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package shared
|
||||||
|
|
||||||
|
type Record struct {
|
||||||
|
DName string `json:"dname,omitempty"`
|
||||||
|
TTL int `json:"ttl,omitempty"`
|
||||||
|
RecordType string `json:"record_type,omitempty"`
|
||||||
|
Data []string `json:"data,omitempty"`
|
||||||
|
LineIndex int `json:"line_index,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZoneRecord struct {
|
||||||
|
LineIndex int `json:"line_index,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
DataB64 []string `json:"data_b64,omitempty"`
|
||||||
|
DNameB64 string `json:"dname_b64,omitempty"`
|
||||||
|
TextB64 string `json:"text_b64,omitempty"`
|
||||||
|
RecordType string `json:"record_type,omitempty"`
|
||||||
|
TTL int `json:"ttl,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZoneSerial struct {
|
||||||
|
NewSerial string `json:"new_serial,omitempty"`
|
||||||
|
}
|
159
providers/dns/cpanel/internal/whm/client.go
Normal file
159
providers/dns/cpanel/internal/whm/client.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
package whm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const statusFailed = 0
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
username string
|
||||||
|
token string
|
||||||
|
|
||||||
|
baseURL *url.URL
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(baseURL string, username string, token string) (*Client, error) {
|
||||||
|
apiEndpoint, err := url.Parse(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
username: username,
|
||||||
|
token: token,
|
||||||
|
baseURL: apiEndpoint.JoinPath("json-api"),
|
||||||
|
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchZoneInformation fetches zone information.
|
||||||
|
// https://api.docs.cpanel.net/openapi/whm/operation/parse_dns_zone/
|
||||||
|
func (c Client) FetchZoneInformation(ctx context.Context, domain string) ([]shared.ZoneRecord, error) {
|
||||||
|
endpoint := c.baseURL.JoinPath("parse_dns_zone")
|
||||||
|
|
||||||
|
query := endpoint.Query()
|
||||||
|
query.Set("zone", domain)
|
||||||
|
endpoint.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
var result APIResponse[ZoneData]
|
||||||
|
|
||||||
|
err := c.doRequest(ctx, endpoint, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Metadata.Result == statusFailed {
|
||||||
|
return nil, toError(result.Metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Data.Payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRecord adds a new record.
|
||||||
|
//
|
||||||
|
// add='{"dname":"example", "ttl":14400, "record_type":"TXT", "data":["string1", "string2"]}'
|
||||||
|
func (c Client) AddRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) {
|
||||||
|
data, err := json.Marshal(record)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request JSON data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.updateZone(ctx, serial, domain, "add", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditRecord edits an existing record.
|
||||||
|
//
|
||||||
|
// edit='{"line_index": 9, "dname":"example", "ttl":14400, "record_type":"TXT", "data":["string1", "string2"]}'
|
||||||
|
func (c Client) EditRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) {
|
||||||
|
data, err := json.Marshal(record)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request JSON data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.updateZone(ctx, serial, domain, "edit", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRecord deletes an existing record.
|
||||||
|
//
|
||||||
|
// remove=22
|
||||||
|
func (c Client) DeleteRecord(ctx context.Context, serial uint32, domain string, lineIndex int) (*shared.ZoneSerial, error) {
|
||||||
|
return c.updateZone(ctx, serial, domain, "remove", strconv.Itoa(lineIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://api.docs.cpanel.net/openapi/whm/operation/mass_edit_dns_zone/
|
||||||
|
func (c Client) updateZone(ctx context.Context, serial uint32, domain, action, data string) (*shared.ZoneSerial, error) {
|
||||||
|
endpoint := c.baseURL.JoinPath("mass_edit_dns_zone")
|
||||||
|
|
||||||
|
query := endpoint.Query()
|
||||||
|
query.Set("serial", strconv.FormatUint(uint64(serial), 10))
|
||||||
|
query.Set(action, data)
|
||||||
|
query.Set("zone", domain)
|
||||||
|
endpoint.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
var result APIResponse[shared.ZoneSerial]
|
||||||
|
|
||||||
|
err := c.doRequest(ctx, endpoint, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Metadata.Result == statusFailed {
|
||||||
|
return nil, toError(result.Metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) doRequest(ctx context.Context, endpoint *url.URL, result any) error {
|
||||||
|
query := endpoint.Query()
|
||||||
|
query.Set("api.version", "1")
|
||||||
|
endpoint.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://api.docs.cpanel.net/whm/tokens/
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("whm %s:%s", c.username, c.token))
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return errutils.NewHTTPDoError(req, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return errutils.NewUnexpectedResponseStatusCodeError(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
|
||||||
|
}
|
170
providers/dns/cpanel/internal/whm/client_test.go
Normal file
170
providers/dns/cpanel/internal/whm/client_test.go
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
package whm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTest(t *testing.T, pattern string, filename string) *Client {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
open, err := os.Open(filepath.Join("fixtures", filename))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = open.Close() }()
|
||||||
|
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
_, err = io.Copy(rw, open)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
client, err := NewClient(server.URL, "user", "secret")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client.HTTPClient = server.Client()
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_FetchZoneInformation(t *testing.T) {
|
||||||
|
client := setupTest(t, "/json-api/parse_dns_zone", "zone-info.json")
|
||||||
|
|
||||||
|
zoneInfo, err := client.FetchZoneInformation(context.Background(), "example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := []shared.ZoneRecord{{
|
||||||
|
LineIndex: 22,
|
||||||
|
Type: "record",
|
||||||
|
DataB64: []string{"dGV4YXMuY29tLg=="},
|
||||||
|
DNameB64: "dGV4YXMuY29tLg==",
|
||||||
|
RecordType: "MX",
|
||||||
|
TTL: 14400,
|
||||||
|
}}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, zoneInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_FetchZoneInformation_error(t *testing.T) {
|
||||||
|
client := setupTest(t, "/json-api/parse_dns_zone", "zone-info_error.json")
|
||||||
|
|
||||||
|
zoneInfo, err := client.FetchZoneInformation(context.Background(), "example.com")
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, zoneInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AddRecord(t *testing.T) {
|
||||||
|
client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone.json")
|
||||||
|
|
||||||
|
record := shared.Record{
|
||||||
|
DName: "example",
|
||||||
|
TTL: 14400,
|
||||||
|
RecordType: "TXT",
|
||||||
|
Data: []string{"string1", "string2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneSerial, err := client.AddRecord(context.Background(), 123456, "example.com", record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &shared.ZoneSerial{NewSerial: "2021031903"}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, zoneSerial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AddRecord_error(t *testing.T) {
|
||||||
|
client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone_error.json")
|
||||||
|
|
||||||
|
record := shared.Record{
|
||||||
|
DName: "example",
|
||||||
|
TTL: 14400,
|
||||||
|
RecordType: "TXT",
|
||||||
|
Data: []string{"string1", "string2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneSerial, err := client.AddRecord(context.Background(), 123456, "example.com", record)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, zoneSerial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_EditRecord(t *testing.T) {
|
||||||
|
client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone.json")
|
||||||
|
|
||||||
|
record := shared.Record{
|
||||||
|
LineIndex: 9,
|
||||||
|
DName: "example",
|
||||||
|
TTL: 14400,
|
||||||
|
RecordType: "TXT",
|
||||||
|
Data: []string{"string1", "string2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneSerial, err := client.EditRecord(context.Background(), 123456, "example.com", record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &shared.ZoneSerial{NewSerial: "2021031903"}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, zoneSerial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_EditRecord_error(t *testing.T) {
|
||||||
|
client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone_error.json")
|
||||||
|
|
||||||
|
record := shared.Record{
|
||||||
|
LineIndex: 9,
|
||||||
|
DName: "example",
|
||||||
|
TTL: 14400,
|
||||||
|
RecordType: "TXT",
|
||||||
|
Data: []string{"string1", "string2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneSerial, err := client.EditRecord(context.Background(), 123456, "example.com", record)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, zoneSerial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_DeleteRecord(t *testing.T) {
|
||||||
|
client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone.json")
|
||||||
|
|
||||||
|
zoneSerial, err := client.DeleteRecord(context.Background(), 123456, "example.com", 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &shared.ZoneSerial{NewSerial: "2021031903"}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, zoneSerial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_DeleteRecord_error(t *testing.T) {
|
||||||
|
client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone_error.json")
|
||||||
|
|
||||||
|
zoneSerial, err := client.DeleteRecord(context.Background(), 123456, "example.com", 0)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, zoneSerial)
|
||||||
|
}
|
11
providers/dns/cpanel/internal/whm/fixtures/update-zone.json
Normal file
11
providers/dns/cpanel/internal/whm/fixtures/update-zone.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"new_serial": "2021031903"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"command": "mass_edit_dns_zone",
|
||||||
|
"reason": "OK",
|
||||||
|
"result": 1,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"data": null,
|
||||||
|
"metadata": {
|
||||||
|
"command": "mass_edit_dns_zone",
|
||||||
|
"reason": "There is a problem",
|
||||||
|
"result": 0,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
}
|
22
providers/dns/cpanel/internal/whm/fixtures/zone-info.json
Normal file
22
providers/dns/cpanel/internal/whm/fixtures/zone-info.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"payload": [
|
||||||
|
{
|
||||||
|
"line_index": 22,
|
||||||
|
"type": "record",
|
||||||
|
"data_b64": [
|
||||||
|
"dGV4YXMuY29tLg=="
|
||||||
|
],
|
||||||
|
"dname_b64": "dGV4YXMuY29tLg==",
|
||||||
|
"record_type": "MX",
|
||||||
|
"ttl": 14400
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"command": "parse_dns_zone",
|
||||||
|
"reason": "OK",
|
||||||
|
"result": 1,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"data": null,
|
||||||
|
"metadata": {
|
||||||
|
"command": "parse_dns_zone",
|
||||||
|
"reason": "There is a problem",
|
||||||
|
"result": 0,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
}
|
27
providers/dns/cpanel/internal/whm/types.go
Normal file
27
providers/dns/cpanel/internal/whm/types.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package whm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIResponse[T any] struct {
|
||||||
|
Metadata Metadata `json:"metadata,omitempty"`
|
||||||
|
Data T `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
Command string `json:"command,omitempty"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Result int `json:"result,omitempty"`
|
||||||
|
Version int `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZoneData struct {
|
||||||
|
Payload []shared.ZoneRecord `json:"payload,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toError(m Metadata) error {
|
||||||
|
return fmt.Errorf("%s error(%d): %s", m.Command, m.Result, m.Reason)
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/go-acme/lego/v4/providers/dns/cloudxns"
|
"github.com/go-acme/lego/v4/providers/dns/cloudxns"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/conoha"
|
"github.com/go-acme/lego/v4/providers/dns/conoha"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/constellix"
|
"github.com/go-acme/lego/v4/providers/dns/constellix"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cpanel"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/derak"
|
"github.com/go-acme/lego/v4/providers/dns/derak"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/desec"
|
"github.com/go-acme/lego/v4/providers/dns/desec"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/designate"
|
"github.com/go-acme/lego/v4/providers/dns/designate"
|
||||||
|
@ -177,6 +178,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
|
||||||
return conoha.NewDNSProvider()
|
return conoha.NewDNSProvider()
|
||||||
case "constellix":
|
case "constellix":
|
||||||
return constellix.NewDNSProvider()
|
return constellix.NewDNSProvider()
|
||||||
|
case "cpanel":
|
||||||
|
return cpanel.NewDNSProvider()
|
||||||
case "derak":
|
case "derak":
|
||||||
return derak.NewDNSProvider()
|
return derak.NewDNSProvider()
|
||||||
case "desec":
|
case "desec":
|
||||||
|
|
Loading…
Reference in a new issue