Add DNS provider for cloud.ru (#1968)

This commit is contained in:
Ludovic Fernandez 2023-07-27 13:09:39 +02:00 committed by GitHub
parent 6c13564bad
commit ae7823705e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1190 additions and 29 deletions

View file

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

View file

@ -31,6 +31,7 @@ func allDNSCodes() string {
"clouddns",
"cloudflare",
"cloudns",
"cloudru",
"cloudxns",
"conoha",
"constellix",
@ -516,6 +517,29 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()
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":
// generated from: providers/dns/cloudxns/cloudxns.toml
ew.writeln(`Configuration for CloudXNS.`)

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

View file

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

View 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")
}

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

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

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

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

View file

@ -0,0 +1,4 @@
{
"error": "invalid_client",
"error_description": "client not found"
}

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

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

View 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
}
]
}

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

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

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

View 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"`
}

View file

@ -22,6 +22,7 @@ import (
"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/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/conoha"
"github.com/go-acme/lego/v4/providers/dns/constellix"
@ -166,6 +167,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return cloudflare.NewDNSProvider()
case "cloudns":
return cloudns.NewDNSProvider()
case "cloudru":
return cloudru.NewDNSProvider()
case "cloudxns":
return cloudxns.NewDNSProvider()
case "conoha":

View file

@ -20,7 +20,7 @@ const (
AuthBaseURL = "https://auth.mythic-beasts.com/login"
)
// Client the EasyDNS API client.
// Client the Mythic Beasts API client.
type Client struct {
username string
password string