mirror of
				https://github.com/go-acme/lego.git
				synced 2025-10-31 16:37:41 +02:00 
			
		
		
		
	Add DNS provider for CPanel and WHM (#1977)
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							719adc3964
						
					
				
				
					commit
					83ff393131
				
			| @@ -237,3 +237,5 @@ issues: | ||||
|       text: 'Duplicate words \(0\) found' | ||||
|     - path: cmd/cmd_renew.go | ||||
|       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/)                          | | ||||
| | [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/)                          | | ||||
| | [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/)                        | | ||||
| | [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/)                | | ||||
| | [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/)            | | ||||
| | [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/)                                  | | ||||
| | [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/)                                | | ||||
| | [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/)                             | | ||||
| | [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/)                         | | ||||
| | [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/)                     | | ||||
| | [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/)         | | ||||
| | [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/)                        | | ||||
| | [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/)                                | | ||||
| | [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/)                              | | ||||
| | [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/)                     | | ||||
| | [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/)                        | | ||||
| | [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/)                      | | ||||
| | [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/)                          | | ||||
| | [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/)                            | | ||||
| | [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/)                                  | | ||||
| | [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/)                      | | ||||
| | [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/)                  | | ||||
| | [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/)                      | | ||||
| | [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/)           | | ||||
| | [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/)                    | | ||||
| | [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/)                        | | ||||
| | [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/)                        | | ||||
| | [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/)                 | | ||||
| | [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/)                            |                                                                                 | | ||||
| | [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/)                        | | ||||
| | [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/)               | | ||||
| | [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/)               | | ||||
| | [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/)                         | | ||||
| | [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/)                 | | ||||
| | [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/)                    | | ||||
| | [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/)                            | | ||||
| | [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/)                          | | ||||
| | [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/)                         | | ||||
| | [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/)          | | ||||
| | [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/)                   | | ||||
| | [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/)                  | | ||||
| | [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/)                       | | ||||
| | [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/)                            | | ||||
| | [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/)                      | | ||||
| | [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/)                            | | ||||
| | [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/)                            | | ||||
| | [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/)                 | | ||||
| | [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/)                            | | ||||
| | [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/)                          | | ||||
| | [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/)                        | | ||||
| | [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/)                      | | ||||
| | [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/)                        | | ||||
| | [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/)                 | | ||||
| | [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/)                              | | ||||
| | [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 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 --> | ||||
|  | ||||
|   | ||||
| @@ -35,6 +35,7 @@ func allDNSCodes() string { | ||||
| 		"cloudxns", | ||||
| 		"conoha", | ||||
| 		"constellix", | ||||
| 		"cpanel", | ||||
| 		"derak", | ||||
| 		"desec", | ||||
| 		"designate", | ||||
| @@ -611,6 +612,31 @@ func displayDNSHelp(w io.Writer, name string) error { | ||||
| 		ew.writeln() | ||||
| 		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": | ||||
| 		// generated from: providers/dns/derak/derak.toml | ||||
| 		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 | ||||
|  | ||||
| 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 | ||||
| """ | ||||
|   | ||||
							
								
								
									
										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/conoha" | ||||
| 	"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/desec" | ||||
| 	"github.com/go-acme/lego/v4/providers/dns/designate" | ||||
| @@ -177,6 +178,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { | ||||
| 		return conoha.NewDNSProvider() | ||||
| 	case "constellix": | ||||
| 		return constellix.NewDNSProvider() | ||||
| 	case "cpanel": | ||||
| 		return cpanel.NewDNSProvider() | ||||
| 	case "derak": | ||||
| 		return derak.NewDNSProvider() | ||||
| 	case "desec": | ||||
|   | ||||
		Reference in New Issue
	
	Block a user