mirror of
https://github.com/go-acme/lego.git
synced 2024-12-26 03:09:37 +02:00
Add DNS provider for cloud.ru (#1968)
This commit is contained in:
parent
6c13564bad
commit
ae7823705e
54
README.md
54
README.md
@ -57,33 +57,33 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
|
|||||||
| [Amazon Route 53](https://go-acme.github.io/lego/dns/route53/) | [ArvanCloud](https://go-acme.github.io/lego/dns/arvancloud/) | [Aurora DNS](https://go-acme.github.io/lego/dns/auroradns/) | [Autodns](https://go-acme.github.io/lego/dns/autodns/) |
|
| [Amazon Route 53](https://go-acme.github.io/lego/dns/route53/) | [ArvanCloud](https://go-acme.github.io/lego/dns/arvancloud/) | [Aurora DNS](https://go-acme.github.io/lego/dns/auroradns/) | [Autodns](https://go-acme.github.io/lego/dns/autodns/) |
|
||||||
| [Azure (deprecated)](https://go-acme.github.io/lego/dns/azure/) | [AzureDNS](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/) | [AzureDNS](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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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](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/) |
|
| [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 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/) |
|
| [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/) |
|
||||||
| [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | [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/) |
|
| [Hosttech](https://go-acme.github.io/lego/dns/hosttech/) | [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | [Hurricane Electric DNS](https://go-acme.github.io/lego/dns/hurricane/) | [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) |
|
||||||
| [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/) |
|
| [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.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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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/) |
|
| [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/) |
|
||||||
| [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 Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) |
|
| [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 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 -->
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ func allDNSCodes() string {
|
|||||||
"clouddns",
|
"clouddns",
|
||||||
"cloudflare",
|
"cloudflare",
|
||||||
"cloudns",
|
"cloudns",
|
||||||
|
"cloudru",
|
||||||
"cloudxns",
|
"cloudxns",
|
||||||
"conoha",
|
"conoha",
|
||||||
"constellix",
|
"constellix",
|
||||||
@ -516,6 +517,29 @@ func displayDNSHelp(w io.Writer, name string) error {
|
|||||||
ew.writeln()
|
ew.writeln()
|
||||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudns`)
|
ew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudns`)
|
||||||
|
|
||||||
|
case "cloudru":
|
||||||
|
// generated from: providers/dns/cloudru/cloudru.toml
|
||||||
|
ew.writeln(`Configuration for Cloud.ru.`)
|
||||||
|
ew.writeln(`Code: 'cloudru'`)
|
||||||
|
ew.writeln(`Since: 'v4.14.0'`)
|
||||||
|
ew.writeln()
|
||||||
|
|
||||||
|
ew.writeln(`Credentials:`)
|
||||||
|
ew.writeln(` - "CLOUDRU_KEY_ID": Key ID (login)`)
|
||||||
|
ew.writeln(` - "CLOUDRU_SECRET": Key Secret`)
|
||||||
|
ew.writeln(` - "CLOUDRU_SERVICE_INSTANCE_ID": Service Instance ID (parentId)`)
|
||||||
|
ew.writeln()
|
||||||
|
|
||||||
|
ew.writeln(`Additional Configuration:`)
|
||||||
|
ew.writeln(` - "CLOUDRU_HTTP_TIMEOUT": API request timeout`)
|
||||||
|
ew.writeln(` - "CLOUDRU_POLLING_INTERVAL": Time between DNS propagation check`)
|
||||||
|
ew.writeln(` - "CLOUDRU_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
|
||||||
|
ew.writeln(` - "CLOUDRU_SEQUENCE_INTERVAL": Time between sequential requests`)
|
||||||
|
ew.writeln(` - "CLOUDRU_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/cloudru`)
|
||||||
|
|
||||||
case "cloudxns":
|
case "cloudxns":
|
||||||
// generated from: providers/dns/cloudxns/cloudxns.toml
|
// generated from: providers/dns/cloudxns/cloudxns.toml
|
||||||
ew.writeln(`Configuration for CloudXNS.`)
|
ew.writeln(`Configuration for CloudXNS.`)
|
||||||
|
72
docs/content/dns/zz_gen_cloudru.md
Normal file
72
docs/content/dns/zz_gen_cloudru.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
title: "Cloud.ru"
|
||||||
|
date: 2019-03-03T16:39:46+01:00
|
||||||
|
draft: false
|
||||||
|
slug: cloudru
|
||||||
|
dnsprovider:
|
||||||
|
since: "v4.14.0"
|
||||||
|
code: "cloudru"
|
||||||
|
url: "https://cloud.ru"
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||||
|
<!-- providers/dns/cloudru/cloudru.toml -->
|
||||||
|
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||||
|
|
||||||
|
|
||||||
|
Configuration for [Cloud.ru](https://cloud.ru).
|
||||||
|
|
||||||
|
|
||||||
|
<!--more-->
|
||||||
|
|
||||||
|
- Code: `cloudru`
|
||||||
|
- Since: v4.14.0
|
||||||
|
|
||||||
|
|
||||||
|
Here is an example bash command using the Cloud.ru provider:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CLOUDRU_SERVICE_INSTANCE_ID=ppp \
|
||||||
|
CLOUDRU_KEY_ID=xxx \
|
||||||
|
CLOUDRU_SECRET=yyy \
|
||||||
|
lego --email you@example.com --dns cloudru --domains my.example.org run
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
|
||||||
|
| Environment Variable Name | Description |
|
||||||
|
|-----------------------|-------------|
|
||||||
|
| `CLOUDRU_KEY_ID` | Key ID (login) |
|
||||||
|
| `CLOUDRU_SECRET` | Key Secret |
|
||||||
|
| `CLOUDRU_SERVICE_INSTANCE_ID` | Service Instance ID (parentId) |
|
||||||
|
|
||||||
|
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 |
|
||||||
|
|--------------------------------|-------------|
|
||||||
|
| `CLOUDRU_HTTP_TIMEOUT` | API request timeout |
|
||||||
|
| `CLOUDRU_POLLING_INTERVAL` | Time between DNS propagation check |
|
||||||
|
| `CLOUDRU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
|
||||||
|
| `CLOUDRU_SEQUENCE_INTERVAL` | Time between sequential requests |
|
||||||
|
| `CLOUDRU_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
|
||||||
|
|
||||||
|
- [API documentation](https://cloud.ru/ru/docs/clouddns/ug/topics/api-ref.html)
|
||||||
|
|
||||||
|
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||||
|
<!-- providers/dns/cloudru/cloudru.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, 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, 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, websupport, wedos, yandex, 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, 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, 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, websupport, wedos, yandex, yandexcloud, zoneee, zonomi
|
||||||
|
|
||||||
More information: https://go-acme.github.io/lego/dns
|
More information: https://go-acme.github.io/lego/dns
|
||||||
"""
|
"""
|
||||||
|
200
providers/dns/cloudru/cloudru.go
Normal file
200
providers/dns/cloudru/cloudru.go
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
// Package cloudru implements a DNS provider for solving the DNS-01 challenge using cloud.ru DNS.
|
||||||
|
package cloudru
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||||
|
"github.com/go-acme/lego/v4/platform/config/env"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cloudru/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Environment variables names.
|
||||||
|
const (
|
||||||
|
envNamespace = "CLOUDRU_"
|
||||||
|
|
||||||
|
EnvServiceInstanceID = envNamespace + "SERVICE_INSTANCE_ID"
|
||||||
|
EnvKeyID = envNamespace + "KEY_ID"
|
||||||
|
EnvSecret = envNamespace + "SECRET"
|
||||||
|
|
||||||
|
EnvTTL = envNamespace + "TTL"
|
||||||
|
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||||
|
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||||
|
EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL"
|
||||||
|
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is used to configure the creation of the DNSProvider.
|
||||||
|
type Config struct {
|
||||||
|
ServiceInstanceID string
|
||||||
|
KeyID string
|
||||||
|
Secret string
|
||||||
|
|
||||||
|
PropagationTimeout time.Duration
|
||||||
|
PollingInterval time.Duration
|
||||||
|
SequenceInterval time.Duration
|
||||||
|
HTTPClient *http.Client
|
||||||
|
TTL int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||||
|
func NewDefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
|
||||||
|
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
|
||||||
|
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),
|
||||||
|
SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),
|
||||||
|
HTTPClient: &http.Client{
|
||||||
|
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSProvider struct {
|
||||||
|
config *Config
|
||||||
|
client *internal.Client
|
||||||
|
records map[string]*internal.Record
|
||||||
|
recordsMu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProvider returns a DNSProvider instance configured for cloud.ru.
|
||||||
|
// Credentials must be passed in the environment variables:
|
||||||
|
// CLOUDRU_SERVICE_INSTANCE_ID, CLOUDRU_KEY_ID, and CLOUDRU_SECRET.
|
||||||
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
values, err := env.Get(EnvServiceInstanceID, EnvKeyID, EnvSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cloudru: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := NewDefaultConfig()
|
||||||
|
config.ServiceInstanceID = values[EnvServiceInstanceID]
|
||||||
|
config.KeyID = values[EnvKeyID]
|
||||||
|
config.Secret = values[EnvSecret]
|
||||||
|
|
||||||
|
return NewDNSProviderConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderConfig return a DNSProvider instance configured for cloud.ru.
|
||||||
|
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("cloudru: the configuration of the DNS provider is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ServiceInstanceID == "" || config.KeyID == "" || config.Secret == "" {
|
||||||
|
return nil, errors.New("cloudru: some credentials information are missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := internal.NewClient(config.KeyID, config.Secret)
|
||||||
|
|
||||||
|
if config.HTTPClient != nil {
|
||||||
|
client.HTTPClient = config.HTTPClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSProvider{
|
||||||
|
config: config,
|
||||||
|
client: client,
|
||||||
|
records: make(map[string]*internal.Record),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record using the specified parameters.
|
||||||
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||||
|
|
||||||
|
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cloudru: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authZone = dns01.UnFqdn(authZone)
|
||||||
|
|
||||||
|
ctx, err := d.client.CreateAuthenticatedContext(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cloudru: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zone, err := d.getZoneInformationByName(ctx, d.config.ServiceInstanceID, authZone)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cloudru: could not find zone information (ServiceInstanceID: %s, zone: %s): %w", d.config.ServiceInstanceID, authZone, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
record := internal.Record{
|
||||||
|
Name: info.EffectiveFQDN,
|
||||||
|
Type: "TXT",
|
||||||
|
Values: []string{info.Value},
|
||||||
|
TTL: strconv.Itoa(d.config.TTL),
|
||||||
|
}
|
||||||
|
|
||||||
|
newRecord, err := d.client.CreateRecord(ctx, zone.ID, record)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cloudru: could not create record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.recordsMu.Lock()
|
||||||
|
d.records[token] = newRecord
|
||||||
|
d.recordsMu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp removes a given record that was generated by Present.
|
||||||
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||||
|
|
||||||
|
d.recordsMu.Lock()
|
||||||
|
record, ok := d.records[token]
|
||||||
|
d.recordsMu.Unlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("cloudru: unknown recordID for %q", info.EffectiveFQDN)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, err := d.client.CreateAuthenticatedContext(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cloudru: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.client.DeleteRecord(ctx, record.ZoneID, record.Name, "TXT")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cloudru: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.recordsMu.Lock()
|
||||||
|
delete(d.records, token)
|
||||||
|
d.recordsMu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sequential All DNS challenges for this provider will be resolved sequentially.
|
||||||
|
// Returns the interval between each iteration.
|
||||||
|
func (d *DNSProvider) Sequential() time.Duration {
|
||||||
|
return d.config.SequenceInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||||
|
// Adjusting here to cope with spikes in propagation times.
|
||||||
|
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||||
|
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSProvider) getZoneInformationByName(ctx context.Context, parentID, name string) (internal.Zone, error) {
|
||||||
|
zs, err := d.client.GetZones(ctx, parentID)
|
||||||
|
if err != nil {
|
||||||
|
return internal.Zone{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, element := range zs {
|
||||||
|
if element.Name == name {
|
||||||
|
return element, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return internal.Zone{}, errors.New("could not find Zone record")
|
||||||
|
}
|
27
providers/dns/cloudru/cloudru.toml
Normal file
27
providers/dns/cloudru/cloudru.toml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
Name = "Cloud.ru"
|
||||||
|
Description = ''''''
|
||||||
|
URL = "https://cloud.ru"
|
||||||
|
Code = "cloudru"
|
||||||
|
Since = "v4.14.0"
|
||||||
|
|
||||||
|
Example = '''
|
||||||
|
CLOUDRU_SERVICE_INSTANCE_ID=ppp \
|
||||||
|
CLOUDRU_KEY_ID=xxx \
|
||||||
|
CLOUDRU_SECRET=yyy \
|
||||||
|
lego --email you@example.com --dns cloudru --domains my.example.org run
|
||||||
|
'''
|
||||||
|
|
||||||
|
[Configuration]
|
||||||
|
[Configuration.Credentials]
|
||||||
|
CLOUDRU_SERVICE_INSTANCE_ID = "Service Instance ID (parentId)"
|
||||||
|
CLOUDRU_KEY_ID = "Key ID (login)"
|
||||||
|
CLOUDRU_SECRET = "Key Secret"
|
||||||
|
[Configuration.Additional]
|
||||||
|
CLOUDRU_POLLING_INTERVAL = "Time between DNS propagation check"
|
||||||
|
CLOUDRU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
|
||||||
|
CLOUDRU_TTL = "The TTL of the TXT record used for the DNS challenge"
|
||||||
|
CLOUDRU_HTTP_TIMEOUT = "API request timeout"
|
||||||
|
CLOUDRU_SEQUENCE_INTERVAL = "Time between sequential requests"
|
||||||
|
|
||||||
|
[Links]
|
||||||
|
API = "https://cloud.ru/ru/docs/clouddns/ug/topics/api-ref.html"
|
176
providers/dns/cloudru/cloudru_test.go
Normal file
176
providers/dns/cloudru/cloudru_test.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
package cloudru
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/platform/tester"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const envDomain = envNamespace + "DOMAIN"
|
||||||
|
|
||||||
|
var envTest = tester.NewEnvTest(
|
||||||
|
EnvServiceInstanceID,
|
||||||
|
EnvKeyID,
|
||||||
|
EnvSecret).
|
||||||
|
WithDomain(envDomain)
|
||||||
|
|
||||||
|
func TestNewDNSProvider(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
envVars map[string]string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "success",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvServiceInstanceID: "123",
|
||||||
|
EnvKeyID: "user",
|
||||||
|
EnvSecret: "secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing credentials",
|
||||||
|
envVars: map[string]string{},
|
||||||
|
expected: "cloudru: some credentials information are missing: CLOUDRU_SERVICE_INSTANCE_ID,CLOUDRU_KEY_ID,CLOUDRU_SECRET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing service instance ID",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvServiceInstanceID: "",
|
||||||
|
EnvKeyID: "user",
|
||||||
|
EnvSecret: "secret",
|
||||||
|
},
|
||||||
|
expected: "cloudru: some credentials information are missing: CLOUDRU_SERVICE_INSTANCE_ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing key ID",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvServiceInstanceID: "123",
|
||||||
|
EnvKeyID: "",
|
||||||
|
EnvSecret: "secret",
|
||||||
|
},
|
||||||
|
expected: "cloudru: some credentials information are missing: CLOUDRU_KEY_ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing secret",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvServiceInstanceID: "123",
|
||||||
|
EnvKeyID: "user",
|
||||||
|
EnvSecret: "",
|
||||||
|
},
|
||||||
|
expected: "cloudru: some credentials information are missing: CLOUDRU_SECRET",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
defer envTest.RestoreEnv()
|
||||||
|
envTest.ClearEnv()
|
||||||
|
|
||||||
|
envTest.Apply(test.envVars)
|
||||||
|
|
||||||
|
p, err := NewDNSProvider()
|
||||||
|
|
||||||
|
if test.expected == "" {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, p)
|
||||||
|
require.NotNil(t, p.config)
|
||||||
|
require.NotNil(t, p.client)
|
||||||
|
} else {
|
||||||
|
require.EqualError(t, err, test.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderConfig(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
serviceInstanceID string
|
||||||
|
keyID string
|
||||||
|
secret string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "success",
|
||||||
|
serviceInstanceID: "123",
|
||||||
|
keyID: "user",
|
||||||
|
secret: "secret",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing credentials",
|
||||||
|
expected: "cloudru: some credentials information are missing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing service instance ID",
|
||||||
|
serviceInstanceID: "",
|
||||||
|
keyID: "user",
|
||||||
|
secret: "secret",
|
||||||
|
expected: "cloudru: some credentials information are missing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing key ID",
|
||||||
|
serviceInstanceID: "123",
|
||||||
|
keyID: "",
|
||||||
|
secret: "secret",
|
||||||
|
expected: "cloudru: some credentials information are missing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing secret",
|
||||||
|
serviceInstanceID: "123",
|
||||||
|
keyID: "user",
|
||||||
|
secret: "",
|
||||||
|
expected: "cloudru: some credentials information are missing",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
config := NewDefaultConfig()
|
||||||
|
config.ServiceInstanceID = test.serviceInstanceID
|
||||||
|
config.KeyID = test.keyID
|
||||||
|
config.Secret = test.secret
|
||||||
|
|
||||||
|
p, err := NewDNSProviderConfig(config)
|
||||||
|
|
||||||
|
if test.expected == "" {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, p)
|
||||||
|
require.NotNil(t, p.config)
|
||||||
|
require.NotNil(t, p.client)
|
||||||
|
} else {
|
||||||
|
require.EqualError(t, err, test.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLivePresent(t *testing.T) {
|
||||||
|
if !envTest.IsLiveTest() {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
envTest.RestoreEnv()
|
||||||
|
provider, err := NewDNSProvider()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.Present(envTest.GetDomain(), "", "123d==")
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLiveCleanUp(t *testing.T) {
|
||||||
|
if !envTest.IsLiveTest() {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
envTest.RestoreEnv()
|
||||||
|
provider, err := NewDNSProvider()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
174
providers/dns/cloudru/internal/client.go
Normal file
174
providers/dns/cloudru/internal/client.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default API endpoints.
|
||||||
|
const (
|
||||||
|
APIBaseURL = "https://console.sbercloud.ru/api/clouddns/v1"
|
||||||
|
AuthBaseURL = "https://auth.iam.sbercloud.ru/auth/system/openid/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client the Cloud.ru API client.
|
||||||
|
type Client struct {
|
||||||
|
keyID string
|
||||||
|
secret string
|
||||||
|
|
||||||
|
APIEndpoint *url.URL
|
||||||
|
AuthEndpoint *url.URL
|
||||||
|
HTTPClient *http.Client
|
||||||
|
|
||||||
|
token *Token
|
||||||
|
muToken sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient Creates a new Client.
|
||||||
|
func NewClient(login, secret string) *Client {
|
||||||
|
apiEndpoint, _ := url.Parse(APIBaseURL)
|
||||||
|
authEndpoint, _ := url.Parse(AuthBaseURL)
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
keyID: login,
|
||||||
|
secret: secret,
|
||||||
|
APIEndpoint: apiEndpoint,
|
||||||
|
AuthEndpoint: authEndpoint,
|
||||||
|
HTTPClient: &http.Client{Timeout: 5 * time.Second},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetZones(ctx context.Context, parentID string) ([]Zone, error) {
|
||||||
|
endpoint := c.APIEndpoint.JoinPath("zones")
|
||||||
|
|
||||||
|
query := endpoint.Query()
|
||||||
|
query.Set("parentId", parentID)
|
||||||
|
endpoint.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var zones APIResponse[Zone]
|
||||||
|
err = c.do(req, &zones)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return zones.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetRecords(ctx context.Context, zoneID string) ([]Record, error) {
|
||||||
|
endpoint := c.APIEndpoint.JoinPath("zones", zoneID, "records")
|
||||||
|
|
||||||
|
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var records APIResponse[Record]
|
||||||
|
err = c.do(req, &records)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return records.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreateRecord(ctx context.Context, zoneID string, record Record) (*Record, error) {
|
||||||
|
endpoint := c.APIEndpoint.JoinPath("zones", zoneID, "records")
|
||||||
|
|
||||||
|
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result Record
|
||||||
|
err = c.do(req, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DeleteRecord(ctx context.Context, zoneID, name, recordType string) error {
|
||||||
|
endpoint := c.APIEndpoint.JoinPath("zones", zoneID, "records", name, recordType)
|
||||||
|
|
||||||
|
req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.do(req, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) do(req *http.Request, result any) error {
|
||||||
|
tok := getToken(req.Context())
|
||||||
|
if tok != nil {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("not logged in")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errutils.NewReadResponseError(req, resp.StatusCode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(raw, result)
|
||||||
|
if err != nil {
|
||||||
|
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
if payload != nil {
|
||||||
|
err := json.NewEncoder(buf).Encode(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request JSON body: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
if payload != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
159
providers/dns/cloudru/internal/client_test.go
Normal file
159
providers/dns/cloudru/internal/client_test.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
mux.HandleFunc(pattern, handler)
|
||||||
|
|
||||||
|
client := NewClient("user", "secret")
|
||||||
|
client.HTTPClient = server.Client()
|
||||||
|
client.APIEndpoint, _ = url.Parse(server.URL)
|
||||||
|
client.token = &Token{
|
||||||
|
AccessToken: "secret",
|
||||||
|
ExpiresIn: 60,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
Deadline: time.Now().Add(1 * time.Minute),
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFixtureHandler(method, filename string) http.HandlerFunc {
|
||||||
|
return func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != method {
|
||||||
|
http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filepath.Join("fixtures", filename))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = file.Close() }()
|
||||||
|
|
||||||
|
_, _ = io.Copy(rw, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_GetZones(t *testing.T) {
|
||||||
|
client := setupTest(t, "/zones", writeFixtureHandler(http.MethodGet, "zones.json"))
|
||||||
|
|
||||||
|
ctx := mockContext()
|
||||||
|
|
||||||
|
zones, err := client.GetZones(ctx, "xxx")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := []Zone{
|
||||||
|
{
|
||||||
|
ID: "59556fcd-95ff-451f-b49b-9732f21f944a",
|
||||||
|
ParentID: "2d7b6194-2b83-4f71-86fd-a1e727e347b2",
|
||||||
|
Name: "example.com",
|
||||||
|
Valid: true,
|
||||||
|
Delegated: true,
|
||||||
|
CreatedAt: time.Date(2023, 7, 23, 8, 12, 41, 0, time.UTC),
|
||||||
|
UpdatedAt: time.Date(2023, 7, 24, 5, 50, 28, 0, time.UTC),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, zones)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_GetRecords(t *testing.T) {
|
||||||
|
client := setupTest(t, "/zones/zzz/records", writeFixtureHandler(http.MethodGet, "records.json"))
|
||||||
|
|
||||||
|
ctx := mockContext()
|
||||||
|
|
||||||
|
records, err := client.GetRecords(ctx, "zzz")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := []Record{
|
||||||
|
{
|
||||||
|
ZoneID: "59556fcd-95ff-451f-b49b-9732f21f944a",
|
||||||
|
Name: "example.com.",
|
||||||
|
Type: "SOA",
|
||||||
|
Values: []string{
|
||||||
|
"cdns-ns01.sbercloud.ru. mail.sbercloud.ru 1 120 3600 604800 3600",
|
||||||
|
},
|
||||||
|
TTL: "3600",
|
||||||
|
Enables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ZoneID: "59556fcd-95ff-451f-b49b-9732f21f944a",
|
||||||
|
Name: "example.com.",
|
||||||
|
Type: "NS",
|
||||||
|
Values: []string{
|
||||||
|
"cdns-ns01.sbercloud.ru.",
|
||||||
|
"cdns-ns02.sbercloud.ru.",
|
||||||
|
},
|
||||||
|
TTL: "3600",
|
||||||
|
Enables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ZoneID: "59556fcd-95ff-451f-b49b-9732f21f944a",
|
||||||
|
Name: "www.example.com.",
|
||||||
|
Type: "A",
|
||||||
|
Values: []string{
|
||||||
|
"8.8.8.8",
|
||||||
|
},
|
||||||
|
TTL: "3600",
|
||||||
|
Enables: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, records)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_CreateRecord(t *testing.T) {
|
||||||
|
client := setupTest(t, "/zones/zzz/records", writeFixtureHandler(http.MethodPost, "record.json"))
|
||||||
|
|
||||||
|
ctx := mockContext()
|
||||||
|
|
||||||
|
recordReq := Record{
|
||||||
|
Name: "www.example.com.",
|
||||||
|
Type: "TXT",
|
||||||
|
Values: []string{"text"},
|
||||||
|
TTL: "3600",
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := client.CreateRecord(ctx, "zzz", recordReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &Record{
|
||||||
|
ZoneID: "59556fcd-95ff-451f-b49b-9732f21f944a",
|
||||||
|
Name: "www.example.com.",
|
||||||
|
Type: "TXT",
|
||||||
|
Values: []string{
|
||||||
|
"txt",
|
||||||
|
},
|
||||||
|
TTL: "3600",
|
||||||
|
Enables: true,
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_DeleteRecord(t *testing.T) {
|
||||||
|
client := setupTest(t, "/zones/zzz/records/example.com/TXT", writeFixtureHandler(http.MethodDelete, "record.json"))
|
||||||
|
|
||||||
|
ctx := mockContext()
|
||||||
|
|
||||||
|
err := client.DeleteRecord(ctx, "zzz", "example.com", "TXT")
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
4
providers/dns/cloudru/internal/fixtures/auth-error.json
Normal file
4
providers/dns/cloudru/internal/fixtures/auth-error.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"error": "invalid_client",
|
||||||
|
"error_description": "client not found"
|
||||||
|
}
|
8
providers/dns/cloudru/internal/fixtures/auth.json
Normal file
8
providers/dns/cloudru/internal/fixtures/auth.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImEyMjM3ZDhhLWQ0ZDQtNDA5Yi04ZTMxLWM3NGJhYTZhM2NjYiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaWFtIl0sImF1dGhfdGltZSI6MTY5MDA1Mzg1MiwiYXpwIjoiOWE0M2I0OTM1ZDRhMDc5NmRkYjE0Mjk0NjUxYjk2NzciLCJlbWFpbCI6ImxlZ29AOTlmN2I5NzItZmZlYS00OTkyLTgyN2EtY2M4MDYzOTg1MmNhLmlhbS5zYmVyY2xvdWQucnUiLCJleHAiOjE2OTAwNTc0NTIsImlhdCI6MTY5MDA1Mzg1MiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmlhbS5zYmVyY2xvdWQucnUvYXV0aC9zeXN0ZW0iLCJqdGkiOiJlYzk0ZWJhNC03NzU2LTRjNjQtYmNmMC0zMzYxODIwNWM5ODkiLCJuYmYiOjE2OTAwNTM4NTIsIm5vbmNlIjoiIiwicmVhbG1fYWNjZXNzIjpudWxsLCJyZXNvdXJjZV9hY2Nlc3MiOnt9LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIHJvbGVzIiwic3ViIjoiZmVhMzRmYTUtZmE5ZS00OTdkLTk5MDAtNjdhYWJjOWNkZWJjIiwic3ViX2lkIjoiZmVhMzRmYTUtZmE5ZS00OTdkLTk5MDAtNjdhYWJjOWNkZWJjIiwic3ViX3R5cGUiOiJzZXJ2aWNlX2FjY291bnQiLCJ0eXAiOiJCZWFyZXIifQ.hhPr-Xr_NbyRwrqGoqeepthWfpfmD47RjzHUwo2lVPkeMiL8AMWzDPRxs-8gns9eTSHZCoAH0RjyrBnTaOrztInM72h8_rIIFr0MMPIIqrUkp2id_alya9eoiSWg_69PzNZ2CKWJDylL8o4Vi9_cSBYp-6H1xNcOAvO4a9xkNCoGGiogjHWNFq64qnS_P6fYY-pl9leuprCeq1GAKPODevHwzmc4gkEZIj_15SUh_ofJRJICgyLmkELQ8a0wDGYmZcdNKiGQDpd7rHaGrOvO1k8IJHfgs5aCMyuHXybTg6AMlodpYs8MBdk6K_VFY-cxSRB8ocq_Q7Hgt9qaRADg2Q",
|
||||||
|
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImEyMjM3ZDhhLWQ0ZDQtNDA5Yi04ZTMxLWM3NGJhYTZhM2NjYiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaWFtIl0sImF1dGhfdGltZSI6MTY5MDA1Mzg1MiwiYXpwIjoiOWE0M2I0OTM1ZDRhMDc5NmRkYjE0Mjk0NjUxYjk2NzciLCJlbWFpbCI6ImxlZ29AOTlmN2I5NzItZmZlYS00OTkyLTgyN2EtY2M4MDYzOTg1MmNhLmlhbS5zYmVyY2xvdWQucnUiLCJleHAiOjE2OTAwNTc0NTIsImlhdCI6MTY5MDA1Mzg1MiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmlhbS5zYmVyY2xvdWQucnUvYXV0aC9zeXN0ZW0iLCJqdGkiOiIxNDRmMDRlNS1jYjZkLTQ2NTktODJhMi0yMmE5MDQwNGZlZjAiLCJuYmYiOjE2OTAwNTM4NTIsIm5vbmNlIjoiIiwicmVhbG1fYWNjZXNzIjpudWxsLCJyZXNvdXJjZV9hY2Nlc3MiOnt9LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIHJvbGVzIiwic3ViIjoiZmVhMzRmYTUtZmE5ZS00OTdkLTk5MDAtNjdhYWJjOWNkZWJjIiwic3ViX2lkIjoiZmVhMzRmYTUtZmE5ZS00OTdkLTk5MDAtNjdhYWJjOWNkZWJjIiwic3ViX3R5cGUiOiJzZXJ2aWNlX2FjY291bnQiLCJ0eXAiOiJJRCJ9.oW9w9X2EBozdY7JTnL6WBPE114BM52ZOaWLkXamJvUOks_F4fRxw5lJIN-LkTwMZ9jE3PsBV2_SueCL5Ry2ISiEXaZeoQ_FPnSkz-CMFDP6Ph2erOvEWQInTIPA6h-ToIhYMZR8lc_kPOmar2mTT8b043FZ6zFDf28PJCCo8snCgA_tIO7R0fNJYT7Hr-UR7LSrE-Sjz7lsgttyDEPH1P4yPm4ZzRLYLcR240p1iGKG9yxtl8IL6uxseS4pUddimaH6jFPhMFLH44PV4O_-uYs74erjoPiroCHiaWQIdDR5GZDoPCbYXQa0knh9hnK1pX6fO-krHeT3RtfuFf5609A",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"not-before-policy": 0,
|
||||||
|
"scope": "openid profile email roles",
|
||||||
|
"token_type": "Bearer"
|
||||||
|
}
|
11
providers/dns/cloudru/internal/fixtures/record.json
Normal file
11
providers/dns/cloudru/internal/fixtures/record.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"zone_id": "59556fcd-95ff-451f-b49b-9732f21f944a",
|
||||||
|
"name": "www.example.com.",
|
||||||
|
"type": "TXT",
|
||||||
|
"values": [
|
||||||
|
"txt"
|
||||||
|
],
|
||||||
|
"ttl": "3600",
|
||||||
|
"enables": true,
|
||||||
|
"readonly": false
|
||||||
|
}
|
38
providers/dns/cloudru/internal/fixtures/records.json
Normal file
38
providers/dns/cloudru/internal/fixtures/records.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"zone_id": "59556fcd-95ff-451f-b49b-9732f21f944a",
|
||||||
|
"name": "example.com.",
|
||||||
|
"type": "SOA",
|
||||||
|
"values": [
|
||||||
|
"cdns-ns01.sbercloud.ru. mail.sbercloud.ru 1 120 3600 604800 3600"
|
||||||
|
],
|
||||||
|
"ttl": "3600",
|
||||||
|
"enables": true,
|
||||||
|
"readonly": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"zone_id": "59556fcd-95ff-451f-b49b-9732f21f944a",
|
||||||
|
"name": "example.com.",
|
||||||
|
"type": "NS",
|
||||||
|
"values": [
|
||||||
|
"cdns-ns01.sbercloud.ru.",
|
||||||
|
"cdns-ns02.sbercloud.ru."
|
||||||
|
],
|
||||||
|
"ttl": "3600",
|
||||||
|
"enables": true,
|
||||||
|
"readonly": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"zone_id": "59556fcd-95ff-451f-b49b-9732f21f944a",
|
||||||
|
"name": "www.example.com.",
|
||||||
|
"type": "A",
|
||||||
|
"values": [
|
||||||
|
"8.8.8.8"
|
||||||
|
],
|
||||||
|
"ttl": "3600",
|
||||||
|
"enables": true,
|
||||||
|
"readonly": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
14
providers/dns/cloudru/internal/fixtures/zones.json
Normal file
14
providers/dns/cloudru/internal/fixtures/zones.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "59556fcd-95ff-451f-b49b-9732f21f944a",
|
||||||
|
"parent_id": "2d7b6194-2b83-4f71-86fd-a1e727e347b2",
|
||||||
|
"name": "example.com",
|
||||||
|
"valid": true,
|
||||||
|
"validation_text": "sbc-verification: 5c86c962-7ee2-4983-b39b-1d9461959d8b",
|
||||||
|
"delegated": true,
|
||||||
|
"created_at": "2023-07-23T08:12:41.000000Z",
|
||||||
|
"updated_at": "2023-07-24T05:50:28.000000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
106
providers/dns/cloudru/internal/identity.go
Normal file
106
providers/dns/cloudru/internal/identity.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type token string
|
||||||
|
|
||||||
|
const tokenKey token = "token"
|
||||||
|
|
||||||
|
// obtainToken Logs into cloud.ru and acquires a bearer token for use in future API calls.
|
||||||
|
// https://cloud.ru/ru/docs/clouddns/ug/topics/api-ref_authentication.html
|
||||||
|
func (c *Client) obtainToken(ctx context.Context) (*Token, error) {
|
||||||
|
data := make(url.Values)
|
||||||
|
data.Set("grant_type", "access_key")
|
||||||
|
data.Set("client_id", c.keyID)
|
||||||
|
data.Set("client_secret", c.secret)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.AuthEndpoint.String(), strings.NewReader(data.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
resp, err := c.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errutils.NewHTTPDoError(req, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, parseError(req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tok := Token{}
|
||||||
|
err = json.Unmarshal(raw, &tok)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.EqualFold(tok.TokenType, "Bearer") {
|
||||||
|
return nil, fmt.Errorf("received unexpected token type: %s", tok.TokenType)
|
||||||
|
}
|
||||||
|
|
||||||
|
tok.Deadline = time.Now().Add(time.Duration(tok.ExpiresIn) * time.Second)
|
||||||
|
|
||||||
|
return &tok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) {
|
||||||
|
c.muToken.Lock()
|
||||||
|
defer c.muToken.Unlock()
|
||||||
|
|
||||||
|
if c.token != nil && time.Now().Before(c.token.Deadline) {
|
||||||
|
// Already authenticated, stop now
|
||||||
|
return context.WithValue(ctx, tokenKey, c.token), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tok, err := c.obtainToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.WithValue(ctx, tokenKey, tok), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseError(req *http.Request, resp *http.Response) error {
|
||||||
|
if resp.StatusCode < 400 || resp.StatusCode > 499 {
|
||||||
|
return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
errResp := &authResponseError{}
|
||||||
|
err := json.Unmarshal(raw, errResp)
|
||||||
|
if err != nil {
|
||||||
|
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%d: %w", resp.StatusCode, errResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getToken(ctx context.Context) *Token {
|
||||||
|
tok, ok := ctx.Value(tokenKey).(*Token)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return tok
|
||||||
|
}
|
92
providers/dns/cloudru/internal/identity_test.go
Normal file
92
providers/dns/cloudru/internal/identity_test.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mockContext() context.Context {
|
||||||
|
return context.WithValue(context.Background(), tokenKey, &Token{AccessToken: "xxx"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenHandler(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodPost {
|
||||||
|
http.Error(rw, fmt.Sprintf("invalid method, got %s want %s", req.Method, http.MethodPost), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := req.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
grantType := req.Form.Get("grant_type")
|
||||||
|
clientID := req.Form.Get("client_id")
|
||||||
|
clientSecret := req.Form.Get("client_secret")
|
||||||
|
|
||||||
|
if clientID != "user" || clientSecret != "secret" || grantType != "access_key" {
|
||||||
|
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = json.NewEncoder(rw).Encode(Token{
|
||||||
|
AccessToken: "xxx",
|
||||||
|
TokenID: "yyy",
|
||||||
|
ExpiresIn: 666,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
Scope: "openid profile email roles",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_obtainToken(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
mux.HandleFunc("/", tokenHandler)
|
||||||
|
|
||||||
|
client := NewClient("user", "secret")
|
||||||
|
client.HTTPClient = server.Client()
|
||||||
|
client.AuthEndpoint, _ = url.Parse(server.URL)
|
||||||
|
|
||||||
|
assert.Nil(t, client.token)
|
||||||
|
|
||||||
|
tok, err := client.obtainToken(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotNil(t, tok)
|
||||||
|
assert.NotZero(t, tok.Deadline)
|
||||||
|
assert.Equal(t, "xxx", tok.AccessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_CreateAuthenticatedContext(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
mux.HandleFunc("/", tokenHandler)
|
||||||
|
|
||||||
|
client := NewClient("user", "secret")
|
||||||
|
client.HTTPClient = server.Client()
|
||||||
|
client.AuthEndpoint, _ = url.Parse(server.URL)
|
||||||
|
|
||||||
|
assert.Nil(t, client.token)
|
||||||
|
|
||||||
|
ctx, err := client.CreateAuthenticatedContext(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tok := getToken(ctx)
|
||||||
|
|
||||||
|
assert.NotNil(t, tok)
|
||||||
|
assert.NotZero(t, tok.Deadline)
|
||||||
|
assert.Equal(t, "xxx", tok.AccessToken)
|
||||||
|
}
|
53
providers/dns/cloudru/internal/types.go
Normal file
53
providers/dns/cloudru/internal/types.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
// The bearer token for use in API requests
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
TokenID string `json:"id_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
// Number in seconds before the expiration
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
NotBeforePolicy int `json:"not-before-policy"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
|
||||||
|
Deadline time.Time `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type authResponseError struct {
|
||||||
|
ErrorMsg string `json:"error"`
|
||||||
|
ErrorDescription string `json:"error_description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a authResponseError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", a.ErrorMsg, a.ErrorDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIResponse[T any] struct {
|
||||||
|
Items []T `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Zone struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
ParentID string `json:"parent_id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Valid bool `json:"valid,omitempty"`
|
||||||
|
ValidationText string `json:"validationText,omitempty"`
|
||||||
|
Delegated bool `json:"delegated,omitempty"`
|
||||||
|
LastCheck time.Time `json:"lastCheck,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Record struct {
|
||||||
|
ZoneID string `json:"zone_id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Values []string `json:"values,omitempty"`
|
||||||
|
TTL string `json:"ttl,omitempty"`
|
||||||
|
Enables bool `json:"enables,omitempty"`
|
||||||
|
}
|
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/cloudns"
|
"github.com/go-acme/lego/v4/providers/dns/cloudns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cloudru"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/cloudxns"
|
"github.com/go-acme/lego/v4/providers/dns/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"
|
||||||
@ -166,6 +167,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
|
|||||||
return cloudflare.NewDNSProvider()
|
return cloudflare.NewDNSProvider()
|
||||||
case "cloudns":
|
case "cloudns":
|
||||||
return cloudns.NewDNSProvider()
|
return cloudns.NewDNSProvider()
|
||||||
|
case "cloudru":
|
||||||
|
return cloudru.NewDNSProvider()
|
||||||
case "cloudxns":
|
case "cloudxns":
|
||||||
return cloudxns.NewDNSProvider()
|
return cloudxns.NewDNSProvider()
|
||||||
case "conoha":
|
case "conoha":
|
||||||
|
@ -20,7 +20,7 @@ const (
|
|||||||
AuthBaseURL = "https://auth.mythic-beasts.com/login"
|
AuthBaseURL = "https://auth.mythic-beasts.com/login"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client the EasyDNS API client.
|
// Client the Mythic Beasts API client.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
|
Loading…
Reference in New Issue
Block a user