1
0
mirror of https://github.com/go-acme/lego.git synced 2026-06-21 06:36:48 +02:00

Add DNS provider for UCloud (#2972)

This commit is contained in:
Ludovic Fernandez
2026-04-11 15:37:01 +02:00
committed by GitHub
parent c4ab05772b
commit ca178943d0
16 changed files with 915 additions and 8 deletions
+7 -7
View File
@@ -274,37 +274,37 @@ If your DNS provider is not supported, please open an [issue](https://github.com
<td><a href="https://go-acme.github.io/lego/dns/timewebcloud/">Timeweb Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/todaynic/">TodayNIC/时代互联</a></td>
<td><a href="https://go-acme.github.io/lego/dns/transip/">TransIP</a></td>
<td><a href="https://go-acme.github.io/lego/dns/ultradns/">Ultradns</a></td>
<td><a href="https://go-acme.github.io/lego/dns/ucloud/">UCloud</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/ultradns/">Ultradns</a></td>
<td><a href="https://go-acme.github.io/lego/dns/uniteddomains/">United-Domains</a></td>
<td><a href="https://go-acme.github.io/lego/dns/variomedia/">Variomedia</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vegadns/">VegaDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vercel/">Vercel</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/vercel/">Vercel</a></td>
<td><a href="https://go-acme.github.io/lego/dns/versio/">Versio.[nl|eu|uk]</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vinyldns/">VinylDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/virtualname/">Virtualname</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vkcloud/">VK Cloud</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/vkcloud/">VK Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/volcengine/">Volcano Engine/火山引擎</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vscale/">Vscale</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vultr/">Vultr</a></td>
<td><a href="https://go-acme.github.io/lego/dns/webnamesca/">webnames.ca</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/webnamesca/">webnames.ca</a></td>
<td><a href="https://go-acme.github.io/lego/dns/webnames/">webnames.ru</a></td>
<td><a href="https://go-acme.github.io/lego/dns/websupport/">Websupport</a></td>
<td><a href="https://go-acme.github.io/lego/dns/wedos/">WEDOS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/westcn/">West.cn/西部数码</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/westcn/">West.cn/西部数码</a></td>
<td><a href="https://go-acme.github.io/lego/dns/yandex360/">Yandex 360</a></td>
<td><a href="https://go-acme.github.io/lego/dns/yandexcloud/">Yandex Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/yandex/">Yandex PDD</a></td>
<td><a href="https://go-acme.github.io/lego/dns/zoneee/">Zone.ee</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/zoneee/">Zone.ee</a></td>
<td><a href="https://go-acme.github.io/lego/dns/zoneedit/">ZoneEdit</a></td>
<td><a href="https://go-acme.github.io/lego/dns/zonomi/">Zonomi</a></td>
<td></td>
<td></td>
</tr></table>
<!-- END DNS PROVIDERS LIST -->
+24
View File
@@ -183,6 +183,7 @@ func allDNSCodes() string {
"timewebcloud",
"todaynic",
"transip",
"ucloud",
"ultradns",
"uniteddomains",
"variomedia",
@@ -3892,6 +3893,29 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/transip`)
case "ucloud":
// generated from: providers/dns/ucloud/ucloud.toml
ew.writeln(`Configuration for UCloud.`)
ew.writeln(`Code: 'ucloud'`)
ew.writeln(`Since: 'v4.34.0'`)
ew.writeln()
ew.writeln(`Credentials:`)
ew.writeln(` - "UCLOUD_PRIVATE_KEY": Private key`)
ew.writeln(` - "UCLOUD_PUBLIC_KEY": Public key`)
ew.writeln()
ew.writeln(`Additional Configuration:`)
ew.writeln(` - "UCLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
ew.writeln(` - "UCLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
ew.writeln(` - "UCLOUD_PROJECT_ID": Project ID`)
ew.writeln(` - "UCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
ew.writeln(` - "UCLOUD_REGION": Region`)
ew.writeln(` - "UCLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/ucloud`)
case "ultradns":
// generated from: providers/dns/ultradns/ultradns.toml
ew.writeln(`Configuration for Ultradns.`)
+72
View File
@@ -0,0 +1,72 @@
---
title: "UCloud"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: ucloud
dnsprovider:
since: "v4.34.0"
code: "ucloud"
url: "https://www.ucloud.cn/"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/ucloud/ucloud.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [UCloud](https://www.ucloud.cn/).
<!--more-->
- Code: `ucloud`
- Since: v4.34.0
Here is an example bash command using the UCloud provider:
```bash
UCLOUD_PUBLIC_KEY="xxx" \
UCLOUD_PRIVATE_KEY="yyy" \
lego --dns ucloud -d '*.example.com' -d example.com run
```
## Credentials
| Environment Variable Name | Description |
|-----------------------|-------------|
| `UCLOUD_PRIVATE_KEY` | Private key |
| `UCLOUD_PUBLIC_KEY` | Public key |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## Additional Configuration
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `UCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
| `UCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
| `UCLOUD_PROJECT_ID` | Project ID |
| `UCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
| `UCLOUD_REGION` | Region |
| `UCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## More information
- [API documentation](https://docs.ucloud.cn/api/udnr-api/README)
- [Go client](https://github.com/ucloud/ucloud-sdk-go)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/ucloud/ucloud.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
+1 -1
View File
@@ -152,7 +152,7 @@ To display the documentation for a specific DNS provider, run:
$ lego dnshelp -c code
Supported DNS providers:
acme-dns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, eurodns, excedo, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, googledomains, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, iwantmyname, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, nearlyfreespeech, neodigit, netcup, netlify, netnod, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, onecloudru, onlinenet, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
acme-dns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, eurodns, excedo, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, googledomains, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, iwantmyname, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, nearlyfreespeech, neodigit, netcup, netlify, netnod, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, onecloudru, onlinenet, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ucloud, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
More information: https://go-acme.github.io/lego/dns
"""
+1
View File
@@ -84,6 +84,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48
github.com/transip/gotransip/v6 v6.26.1
github.com/ucloud/ucloud-sdk-go v0.22.61
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419
github.com/urfave/cli/v2 v2.27.7
github.com/vinyldns/go-vinyldns v0.9.17
+3
View File
@@ -869,6 +869,7 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -944,6 +945,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1
github.com/transip/gotransip/v6 v6.26.1 h1:MeqIjkTBBsZwWAK6giZyMkqLmKMclVHEuTNmoBdx4MA=
github.com/transip/gotransip/v6 v6.26.1/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ucloud/ucloud-sdk-go v0.22.61 h1:wFMLEvuUQJTqf3serKG8coIt80O2oY85M/MK2/dEG0o=
github.com/ucloud/ucloud-sdk-go v0.22.61/go.mod h1:dyLmFHmUfgb4RZKYQP9IArlvQ2pxzFthfhwxRzOEPIw=
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 h1:/VaznPrb/b68e3iMvkr27fU7JqPKU4j7tIITZnjQX1k=
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419/go.mod h1:QN0/PdenvYWB0GRMz6JJbPeZz2Lph2iys1p8AFVHm2c=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
+102
View File
@@ -0,0 +1,102 @@
package internal
import (
"io"
"github.com/ucloud/ucloud-sdk-go/ucloud"
"github.com/ucloud/ucloud-sdk-go/ucloud/auth"
)
type Client struct {
*ucloud.Client
}
func NewClient(config *ucloud.Config, credential *auth.Credential) *Client {
client := ucloud.NewClientWithMeta(config, credential, ucloud.ClientMeta{Product: "UDNR"})
client.GetLogger().SetOutput(io.Discard)
return &Client{
Client: client,
}
}
func (c *Client) NewDomainDNSAddRequest() *DomainDNSAddRequest {
req := &DomainDNSAddRequest{}
// setup request with client config
c.SetupRequest(req)
// setup retryable with default retry policy (retry for non-create action and common error)
req.SetRetryable(false)
return req
}
func (c *Client) NewDeleteDNSRecordRequest() *DeleteDNSRecordRequest {
req := &DeleteDNSRecordRequest{}
// setup request with client config
c.SetupRequest(req)
// setup retryable with default retry policy (retry for non-create action and common error)
req.SetRetryable(false)
return req
}
func (c *Client) NewDomainDNSQueryRequest() *DomainDNSQueryRequest {
req := &DomainDNSQueryRequest{}
// setup request with client config
c.SetupRequest(req)
// setup retryable with default retry policy (retry for non-create action and common error)
req.SetRetryable(false)
return req
}
// DomainDNSAdd adds a DNS record to a domain.
// https://docs.ucloud.cn/api/udnr-api/udnr_domain_dns_add
func (c *Client) DomainDNSAdd(req *DomainDNSAddRequest) (*DomainDNSAddResponse, error) {
var res DomainDNSAddResponse
reqCopier := *req
err := c.InvokeAction("UdnrDomainDNSAdd", &reqCopier, &res)
if err != nil {
return &res, err
}
return &res, nil
}
// DeleteDNSRecord delete a DNS record.
// https://docs.ucloud.cn/api/udnr-api/udnr_delete_dns_record
func (c *Client) DeleteDNSRecord(req *DeleteDNSRecordRequest) (*DeleteDNSRecordResponse, error) {
var res DeleteDNSRecordResponse
reqCopier := *req
err := c.InvokeAction("UdnrDeleteDnsRecord", &reqCopier, &res)
if err != nil {
return &res, err
}
return &res, nil
}
// DomainDNSQuery gets DNS records of a domain.
// https://docs.ucloud.cn/api/udnr-api/udnr_domain_dns_query
func (c *Client) DomainDNSQuery(req *DomainDNSQueryRequest) (*DomainDNSQueryResponse, error) {
var res DomainDNSQueryResponse
reqCopier := *req
err := c.InvokeAction("UdnrDomainDNSQuery", &reqCopier, &res)
if err != nil {
return &res, err
}
return &res, nil
}
@@ -0,0 +1,122 @@
package internal
import (
"net/http/httptest"
"testing"
"github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/go-acme/lego/v4/providers/dns/internal/useragent"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ucloud/ucloud-sdk-go/ucloud"
"github.com/ucloud/ucloud-sdk-go/ucloud/auth"
)
func mockBuilder() *servermock.Builder[*Client] {
return servermock.NewBuilder[*Client](
func(server *httptest.Server) (*Client, error) {
credential := auth.NewCredential()
credential.PublicKey = "pubkey"
credential.PrivateKey = "privkey"
cfg := ucloud.NewConfig()
cfg.UserAgent = useragent.Get()
cfg.BaseUrl = server.URL
client := NewClient(&cfg, &credential)
client.SetTransport(server.Client().Transport)
return client, nil
},
servermock.CheckHeader().
WithRegexp("U-Timestamp-Ms", `\d+`).
WithContentTypeFromURLEncoded(),
)
}
func TestClient_DomainDNSAdd(t *testing.T) {
client := mockBuilder().
Route("POST /",
servermock.ResponseFromFixture("udnrDomainDNSAdd.json"),
servermock.CheckQueryParameter().Strict().
With("Action", "UdnrDomainDNSAdd"),
servermock.CheckForm().Strict().
WithRegexp("Action", "UdnrDomainDNSAdd").
With("Dn", "example.com").
With("RecordName", "_acme-challenge.example.com").
With("TTL", "600").
With("DnsType", "TXT").
With("Content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY").
With("PublicKey", "pubkey").
WithRegexp("Signature", ".+"),
).
Build(t)
request := client.NewDomainDNSAddRequest()
request.Dn = ucloud.String("example.com")
request.RecordName = ucloud.String("_acme-challenge.example.com")
request.DnsType = ucloud.String("TXT")
request.Content = ucloud.String("ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY")
request.TTL = ucloud.String("600")
_, err := client.DomainDNSAdd(request)
require.NoError(t, err)
}
func TestClient_DomainDNSQuery(t *testing.T) {
client := mockBuilder().
Route("POST /",
servermock.ResponseFromFixture("udnrDomainDNSQuery.json"),
servermock.CheckQueryParameter().Strict().
With("Action", "UdnrDomainDNSQuery"),
servermock.CheckForm().Strict().
WithRegexp("Action", "UdnrDomainDNSQuery").
With("Dn", "example.com").
With("PublicKey", "pubkey").
WithRegexp("Signature", ".+"),
).
Build(t)
request := client.NewDomainDNSQueryRequest()
request.Dn = ucloud.String("example.com")
domains, err := client.DomainDNSQuery(request)
require.NoError(t, err)
expected := []DomainDNSRecord{{
Type: "TXT",
Name: "_acme-challenge.example.com",
Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
Priority: "",
TTL: "600",
}}
assert.Equal(t, expected, domains.Data)
}
func TestClient_DeleteDNSRecord(t *testing.T) {
client := mockBuilder().
Route("POST /",
servermock.ResponseFromFixture("udnrDeleteDNSRecord.json"),
servermock.CheckQueryParameter().Strict().
With("Action", "UdnrDeleteDnsRecord"),
servermock.CheckForm().Strict().
WithRegexp("Action", "UdnrDeleteDnsRecord").
With("Dn", "example.com").
With("RecordName", "_acme-challenge.example.com").
With("DnsType", "TXT").
With("Content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY").
With("PublicKey", "pubkey").
WithRegexp("Signature", ".+"),
).
Build(t)
request := client.NewDeleteDNSRecordRequest()
request.Dn = ucloud.String("example.com")
request.RecordName = ucloud.String("_acme-challenge.example.com")
request.DnsType = ucloud.String("TXT")
request.Content = ucloud.String("ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY")
_, err := client.DeleteDNSRecord(request)
require.NoError(t, err)
}
@@ -0,0 +1,6 @@
{
"Action": "UdnrDeleteDnsRecordResponse",
"Data": {},
"Message": "EygAdLFr",
"RetCode": 0
}
@@ -0,0 +1,6 @@
{
"Action": "UdnrDomainDNSAddResponse",
"Data": "BFtLtuKv",
"Message": "CardVtDd",
"RetCode": 0
}
@@ -0,0 +1,13 @@
{
"Action": "UdnrDomainDNSQueryResponse",
"Data": [
{
"DnsType": "TXT",
"RecordName": "_acme-challenge.example.com",
"Content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
"TTL": "600"
}
],
"Message": "YWMskZFN",
"RetCode": 0
}
+54
View File
@@ -0,0 +1,54 @@
package internal
import (
"github.com/ucloud/ucloud-sdk-go/ucloud/request"
"github.com/ucloud/ucloud-sdk-go/ucloud/response"
)
type DomainDNSAddRequest struct {
request.CommonBase
Dn *string
RecordName *string
DnsType *string //nolint:revive // Because the struct names are used directly.
Content *string
TTL *string
Prio *string
}
type DomainDNSAddResponse struct {
response.CommonBase
}
type DomainDNSQueryRequest struct {
request.CommonBase
Dn *string
}
type DomainDNSQueryResponse struct {
response.CommonBase
Data []DomainDNSRecord
}
type DomainDNSRecord struct {
Type string `json:"DnsType,omitempty"`
Name string `json:"RecordName,omitempty"`
Content string `json:"Content,omitempty"`
Priority string `json:"Prio,omitempty"`
TTL string `json:"TTL,omitempty"`
}
type DeleteDNSRecordRequest struct {
request.CommonBase
Dn *string
RecordName *string
DnsType *string //nolint:revive // Because the struct names are used directly.
Content *string
}
type DeleteDNSRecordResponse struct {
response.CommonBase
}
+191
View File
@@ -0,0 +1,191 @@
// Package ucloud implements a DNS provider for solving the DNS-01 challenge using UCloud.
package ucloud
import (
"errors"
"fmt"
"strconv"
"time"
"github.com/go-acme/lego/v4/challenge"
"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/internal/useragent"
"github.com/go-acme/lego/v4/providers/dns/ucloud/internal"
"github.com/ucloud/ucloud-sdk-go/ucloud"
"github.com/ucloud/ucloud-sdk-go/ucloud/auth"
)
// Environment variables names.
const (
envNamespace = "UCLOUD_"
EnvPublicKey = envNamespace + "PUBLIC_KEY"
EnvPrivateKey = envNamespace + "PRIVATE_KEY"
EnvRegion = envNamespace + "REGION"
EnvProjectID = envNamespace + "PROJECT_ID"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
// Config is used to configure the creation of the DNSProvider.
type Config struct {
Region string
ProjectID string
PublicKey string
PrivateKey string
// only for test
baseURL string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
// NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 600),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
// DNSProvider implements the challenge.Provider interface.
type DNSProvider struct {
config *Config
client *internal.Client
}
// NewDNSProvider returns a DNSProvider instance configured for UCloud.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvPublicKey, EnvPrivateKey)
if err != nil {
return nil, fmt.Errorf("ucloud: %w", err)
}
config := NewDefaultConfig()
config.PublicKey = values[EnvPublicKey]
config.PrivateKey = values[EnvPrivateKey]
config.Region = env.GetOrFile(EnvRegion)
config.ProjectID = env.GetOrFile(EnvProjectID)
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for UCloud.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("ucloud: the configuration of the DNS provider is nil")
}
if config.PublicKey == "" || config.PrivateKey == "" {
return nil, errors.New("ucloud: credentials missing")
}
credential := auth.NewCredential()
credential.PublicKey = config.PublicKey
credential.PrivateKey = config.PrivateKey
cfg := ucloud.NewConfig()
cfg.UserAgent = useragent.Get()
if config.baseURL != "" {
cfg.BaseUrl = config.baseURL
}
if config.Region != "" {
cfg.Region = config.Region
}
if config.ProjectID != "" {
cfg.ProjectId = config.ProjectID
}
return &DNSProvider{
config: config,
client: internal.NewClient(&cfg, &credential),
}, 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("ucloud: could not find zone for domain %q: %w", domain, err)
}
addRequest := d.client.NewDomainDNSAddRequest()
addRequest.Dn = ucloud.String(dns01.UnFqdn(authZone))
addRequest.RecordName = ucloud.String(dns01.UnFqdn(info.EffectiveFQDN))
addRequest.DnsType = ucloud.String("TXT")
addRequest.Content = ucloud.String(info.Value)
addRequest.TTL = ucloud.String(strconv.Itoa(d.config.TTL))
addRequest.WithTimeout(d.config.HTTPTimeout)
_, err = d.client.DomainDNSAdd(addRequest)
if err != nil {
return fmt.Errorf("ucloud: domain DNS add: %w", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("ucloud: could not find zone for domain %q: %w", domain, err)
}
queryRequest := d.client.NewDomainDNSQueryRequest()
queryRequest.Dn = ucloud.String(dns01.UnFqdn(authZone))
queryRequest.WithTimeout(d.config.HTTPTimeout)
dom, err := d.client.DomainDNSQuery(queryRequest)
if err != nil {
return fmt.Errorf("ucloud: domain DNS query: %w", err)
}
for _, record := range dom.Data {
if record.Type != "TXT" || record.Name != dns01.UnFqdn(info.EffectiveFQDN) || record.Content != info.Value {
continue
}
deleteRequest := d.client.NewDeleteDNSRecordRequest()
deleteRequest.Dn = ucloud.String(dns01.UnFqdn(authZone))
deleteRequest.RecordName = ucloud.String(dns01.UnFqdn(info.EffectiveFQDN))
deleteRequest.DnsType = ucloud.String(record.Type)
deleteRequest.Content = ucloud.String(record.Content)
deleteRequest.WithTimeout(d.config.HTTPTimeout)
_, err = d.client.DeleteDNSRecord(deleteRequest)
if err != nil {
return fmt.Errorf("ucloud: delete DNS record: %w", err)
}
}
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
+27
View File
@@ -0,0 +1,27 @@
Name = "UCloud"
Description = ''''''
URL = "https://www.ucloud.cn/"
Code = "ucloud"
Since = "v4.34.0"
Example = '''
UCLOUD_PUBLIC_KEY="xxx" \
UCLOUD_PRIVATE_KEY="yyy" \
lego --dns ucloud -d '*.example.com' -d example.com run
'''
[Configuration]
[Configuration.Credentials]
UCLOUD_PUBLIC_KEY = "Public key"
UCLOUD_PRIVATE_KEY = "Private key"
[Configuration.Additional]
UCLOUD_REGION = "Region"
UCLOUD_PROJECT_ID = "Project ID"
UCLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
UCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
UCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)"
UCLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
[Links]
API = "https://docs.ucloud.cn/api/udnr-api/README"
GoClient = "https://github.com/ucloud/ucloud-sdk-go"
+283
View File
@@ -0,0 +1,283 @@
package ucloud
import (
"net/http"
"net/http/httptest"
"slices"
"testing"
"github.com/go-acme/lego/v4/platform/tester"
"github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
const envDomain = envNamespace + "DOMAIN"
var envTest = tester.NewEnvTest(
EnvPrivateKey,
EnvPublicKey,
EnvRegion,
EnvProjectID,
).WithDomain(envDomain)
func TestNewDNSProvider(t *testing.T) {
testCases := []struct {
desc string
envVars map[string]string
expected string
}{
{
desc: "success",
envVars: map[string]string{
EnvPrivateKey: "xxx",
EnvPublicKey: "yyy",
},
},
{
desc: "missing private key",
envVars: map[string]string{
EnvPrivateKey: "",
EnvPublicKey: "yyy",
},
expected: "ucloud: some credentials information are missing: UCLOUD_PRIVATE_KEY",
},
{
desc: "missing public key",
envVars: map[string]string{
EnvPrivateKey: "xxx",
EnvPublicKey: "",
},
expected: "ucloud: some credentials information are missing: UCLOUD_PUBLIC_KEY",
},
{
desc: "missing credentials",
envVars: map[string]string{},
expected: "ucloud: some credentials information are missing: UCLOUD_PUBLIC_KEY,UCLOUD_PRIVATE_KEY",
},
}
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
privateKey string
publicKey string
expected string
}{
{
desc: "success",
privateKey: "xxx",
publicKey: "yyy",
},
{
desc: "missing private key",
publicKey: "yyy",
expected: "ucloud: credentials missing",
},
{
desc: "missing public key",
privateKey: "xxx",
expected: "ucloud: credentials missing",
},
{
desc: "missing credentials",
expected: "ucloud: credentials missing",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig()
config.PrivateKey = test.privateKey
config.PublicKey = test.publicKey
p, err := NewDNSProviderConfig(config)
if test.expected == "" {
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.config)
require.NotNil(t, p.client)
} else {
require.EqualError(t, err, test.expected)
}
})
}
}
func TestLivePresent(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
}
envTest.RestoreEnv()
provider, err := NewDNSProvider()
require.NoError(t, err)
err = provider.Present(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
func TestLiveCleanUp(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
}
envTest.RestoreEnv()
provider, err := NewDNSProvider()
require.NoError(t, err)
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
func mockBuilder() *servermock.Builder[*DNSProvider] {
return servermock.NewBuilder(
func(server *httptest.Server) (*DNSProvider, error) {
config := NewDefaultConfig()
config.PrivateKey = "privkey"
config.PublicKey = "pubkey"
config.baseURL = server.URL
p, err := NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
p.client.SetTransport(server.Client().Transport)
return p, nil
},
servermock.CheckHeader().
WithRegexp("U-Timestamp-Ms", `\d+`).
WithContentTypeFromURLEncoded(),
)
}
func TestDNSProvider_Present(t *testing.T) {
provider := mockBuilder().
Route("/",
subRouter().
Route(matchAction("UdnrDomainDNSAdd"),
servermock.ResponseFromInternal("udnrDomainDNSAdd.json"),
servermock.CheckQueryParameter().Strict().
With("Action", "UdnrDomainDNSAdd"),
servermock.CheckForm().Strict().
WithRegexp("Action", "UdnrDomainDNSAdd").
With("Dn", "example.com").
With("RecordName", "_acme-challenge.example.com").
With("TTL", "600").
With("DnsType", "TXT").
With("Content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY").
With("PublicKey", "pubkey").
WithRegexp("Signature", ".+"),
),
).
Build(t)
err := provider.Present("example.com", "abc", "123d==")
require.NoError(t, err)
}
func TestDNSProvider_CleanUp(t *testing.T) {
provider := mockBuilder().
Route("/",
subRouter().
Route(matchAction("UdnrDomainDNSQuery"),
servermock.ResponseFromInternal("udnrDomainDNSQuery.json"),
servermock.CheckQueryParameter().Strict().
With("Action", "UdnrDomainDNSQuery"),
servermock.CheckForm().Strict().
WithRegexp("Action", "UdnrDomainDNSQuery").
With("Dn", "example.com").
With("PublicKey", "pubkey").
WithRegexp("Signature", ".+"),
).
Route(matchAction("UdnrDeleteDnsRecord"),
servermock.ResponseFromInternal("udnrDeleteDNSRecord.json"),
servermock.CheckQueryParameter().Strict().
With("Action", "UdnrDeleteDnsRecord"),
servermock.CheckForm().Strict().
WithRegexp("Action", "UdnrDeleteDnsRecord").
With("Dn", "example.com").
With("RecordName", "_acme-challenge.example.com").
With("DnsType", "TXT").
With("Content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY").
With("PublicKey", "pubkey").
WithRegexp("Signature", ".+"),
),
).
Build(t)
err := provider.CleanUp("example.com", "abc", "123d==")
require.NoError(t, err)
}
func matchAction(action string) func(req *http.Request) bool {
return func(req *http.Request) bool {
return req.URL.Query().Get("Action") == action
}
}
// NOTE(ldez): this idea can be reused.
type subRoute struct {
matcher func(req *http.Request) bool
handler http.Handler
}
// NOTE(ldez): this idea can be reused.
type sr struct {
subRoutes []*subRoute
}
func subRouter() *sr {
return &sr{}
}
func (s *sr) Route(matcher func(req *http.Request) bool, handler http.Handler, chain ...servermock.Link) *sr {
for _, link := range slices.Backward(chain) {
handler = link.Bind(handler)
}
s.subRoutes = append(s.subRoutes, &subRoute{
matcher: matcher,
handler: handler,
})
return s
}
func (s *sr) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
for _, route := range s.subRoutes {
if route.matcher(req) {
route.handler.ServeHTTP(rw, req)
return
}
}
rw.WriteHeader(http.StatusNotFound)
}
+3
View File
@@ -177,6 +177,7 @@ import (
"github.com/go-acme/lego/v4/providers/dns/timewebcloud"
"github.com/go-acme/lego/v4/providers/dns/todaynic"
"github.com/go-acme/lego/v4/providers/dns/transip"
"github.com/go-acme/lego/v4/providers/dns/ucloud"
"github.com/go-acme/lego/v4/providers/dns/ultradns"
"github.com/go-acme/lego/v4/providers/dns/uniteddomains"
"github.com/go-acme/lego/v4/providers/dns/variomedia"
@@ -547,6 +548,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return todaynic.NewDNSProvider()
case "transip":
return transip.NewDNSProvider()
case "ucloud":
return ucloud.NewDNSProvider()
case "ultradns":
return ultradns.NewDNSProvider()
case "uniteddomains":