Skip to content

Commit 7504767

Browse files
CLOUDNS: add registrar support (#3961)
ClouDNS uses the same API for DNS and registrar-related operations. Following the pattern I saw in other provider/registrar combos (autodns, cscglobal), I implemented the registry call in the `cloudnsProvider` object type, and when used as a registry it creates an additional instance of the Provider. _Incidentally this causes rate-limiting functionality not to function optimally because each of the instances—the provider and registrar—have their own requestLimit object, causing the rate limit to be tracked and applied within each separately. I'm going to follow up with a PR that will improve the API rate-limit implementation for ClouDNS._ The ClouDNS nameserver API responds with different structures apparently depending on the number of name servers that the domain has. I observed three on the same domain: 1. When there are 2 domain names for non-ClouDNS name servers, the response is an array of name server strings `["abc.ns.cloudflare.net", "def.ns.cloudflare.net"]` 2. When there are 4 ClouDNS name servers, the response is an object with string keys containing the 4 entries `{"1":"pns1.cloudns.net","2":"pns2.cloudns.net","3":"pns3.cloudns.net","4":"pns4.cloudns.net"}` 3. When there are 5 name servers, the response is an object with string keys containing 8 entries included 3 blank values `{"1":"pns1.cloudns.net","2":"pns2.cloudns.net","3":"pns3.cloudns.net","4":"pns4.cloudns.net","5":"abc.ns.cloudflare.net","6":"","7":"","8":""}` This covers all of those cases, plus the possible case where the array contains blank strings. The new `setNameservers` API call needed to send multiple copies of the same key `nameservers[]` in the query value, which is supported by the http query values object, but was not possible with the existing call pattern because the `get` function was built to accept a string map instead of a query values object. So I refactored `get` into a wrapper function that converts the map into a query values object, which calls the new `getWithQuery` function with the query values object. --------- Co-authored-by: Tom Limoncelli <[email protected]>
1 parent d50aef0 commit 7504767

File tree

3 files changed

+107
-10
lines changed

3 files changed

+107
-10
lines changed

documentation/provider/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Jump to a table:
3535
| [`BIND`](bind.md) ||||
3636
| [`BUNNY_DNS`](bunny_dns.md) ||||
3737
| [`CLOUDFLAREAPI`](cloudflareapi.md) ||||
38-
| [`CLOUDNS`](cloudns.md) ||| |
38+
| [`CLOUDNS`](cloudns.md) ||| |
3939
| [`CNR`](cnr.md) ||||
4040
| [`CSCGLOBAL`](cscglobal.md) ||||
4141
| [`DESEC`](desec.md) ||||

providers/cloudns/api.go

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"net/http"
9+
"net/url"
910
"strconv"
1011
"strings"
1112
"sync"
@@ -255,7 +256,7 @@ func (c *cloudnsProvider) isDnssecEnabled(id string) (bool, error) {
255256
if errResp.Description == "The DNSSEC is not active." {
256257
return false, nil
257258
}
258-
return false, fmt.Errorf("failed fetching DS records from ClouDNS: %w", err)
259+
return false, fmt.Errorf("failed fetching DS records from ClouDNS: %s", errResp.Description)
259260
}
260261
}
261262

@@ -280,21 +281,78 @@ func (c *cloudnsProvider) setDnssec(id string, enabled bool) error {
280281
return nil
281282
}
282283

284+
func (c *cloudnsProvider) getNameservers(domain string) ([]string, error) {
285+
params := requestParams{"domain-name": domain}
286+
287+
bodyString, err := c.get("/domains/get-nameservers.json", params)
288+
if err != nil {
289+
return nil, fmt.Errorf("failed fetching nameservers from ClouDNS: %w", err)
290+
}
291+
292+
// The API response might be an array of strings, or an object with string keys "1", "2", etc.
293+
// And sometimes it contains values that are blank strings, which we don't want to include in the result.
294+
295+
// Try as array first
296+
var arr []string
297+
errFromArray := json.Unmarshal(bodyString, &arr)
298+
if errFromArray == nil {
299+
// Filter out empty strings
300+
nameservers := make([]string, 0, len(arr))
301+
for _, ns := range arr {
302+
if ns != "" {
303+
nameservers = append(nameservers, ns)
304+
}
305+
}
306+
return nameservers, nil
307+
}
308+
309+
// Try as map
310+
var nr map[string]string
311+
if err := json.Unmarshal(bodyString, &nr); err != nil {
312+
return nil, fmt.Errorf("failed to unmarshal nameservers from ClouDNS: when attempting object %w, when attempting array %w", err, errFromArray)
313+
}
314+
315+
nameservers := make([]string, 0, len(nr))
316+
for _, ns := range nr {
317+
if ns != "" {
318+
nameservers = append(nameservers, ns)
319+
}
320+
}
321+
322+
return nameservers, nil
323+
}
324+
325+
func (c *cloudnsProvider) setNameservers(domain string, nameservers []string) error {
326+
q := url.Values{}
327+
q.Add("domain-name", domain)
328+
for _, ns := range nameservers {
329+
q.Add("nameservers[]", ns)
330+
}
331+
332+
if _, err := c.getWithQuery("/domains/set-nameservers.json", q); err != nil {
333+
return fmt.Errorf("failed setting nameservers at ClouDNS: %w", err)
334+
}
335+
return nil
336+
}
337+
283338
func (c *cloudnsProvider) get(endpoint string, params requestParams) ([]byte, error) {
339+
q := url.Values{}
340+
for pName, pValue := range params {
341+
q.Add(pName, pValue)
342+
}
343+
return c.getWithQuery(endpoint, q)
344+
}
345+
346+
func (c *cloudnsProvider) getWithQuery(endpoint string, q url.Values) ([]byte, error) {
284347
client := &http.Client{}
285348
req, _ := http.NewRequest(http.MethodGet, "https://api.cloudns.net"+endpoint, nil)
286-
q := req.URL.Query()
287349

288350
// TODO: Support sub-auth-user https://asia.cloudns.net/wiki/article/42/
289351
// Add auth params
290352
q.Add("auth-id", c.creds.id)
291353
q.Add("auth-password", c.creds.password)
292354
q.Add("sub-auth-id", c.creds.subid)
293355

294-
for pName, pValue := range params {
295-
q.Add(pName, pValue)
296-
}
297-
298356
req.URL.RawQuery = q.Encode()
299357

300358
// ClouDNS has an undocumented rate limit

providers/cloudns/cloudnsProvider.go

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"sort"
78
"strconv"
89
"strings"
910

@@ -24,8 +25,7 @@ Info required in `creds.json`:
2425
- auth-password
2526
*/
2627

27-
// NewCloudns creates the provider.
28-
func NewCloudns(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
28+
func NewCloudns(m map[string]string) (*cloudnsProvider, error) {
2929
c := &cloudnsProvider{}
3030
c.requestLimit = NewAdaptiveLimiter(10, 10)
3131

@@ -38,6 +38,14 @@ func NewCloudns(m map[string]string, metadata json.RawMessage) (providers.DNSSer
3838
return c, nil
3939
}
4040

41+
func NewDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
42+
return NewCloudns(conf)
43+
}
44+
45+
func NewReg(conf map[string]string) (providers.Registrar, error) {
46+
return NewCloudns(conf)
47+
}
48+
4149
var features = providers.DocumentationNotes{
4250
// The default for unlisted capabilities is 'Cannot'.
4351
// See providers/capabilities.go for the entire list of capabilities.
@@ -69,10 +77,11 @@ func init() {
6977
const providerName = "CLOUDNS"
7078
const providerMaintainer = "@pragmaton"
7179
fns := providers.DspFuncs{
72-
Initializer: NewCloudns,
80+
Initializer: NewDsp,
7381
RecordAuditor: AuditRecords,
7482
}
7583
providers.RegisterDomainServiceProviderType(providerName, fns, features)
84+
providers.RegisterRegistrarType(providerName, NewReg)
7685
providers.RegisterCustomRecordType("CLOUDNS_WR", providerName, "")
7786
providers.RegisterMaintainer(providerName, providerMaintainer)
7887
}
@@ -268,6 +277,36 @@ func (c *cloudnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
268277
return corrections, actualChangeCount, nil
269278
}
270279

280+
// GetRegistrarCorrections returns corrections to update domain nameserver delegation
281+
func (c *cloudnsProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
282+
// 1. Get current nameservers from registrar
283+
existing, err := c.getNameservers(dc.Name)
284+
if err != nil {
285+
return nil, err
286+
}
287+
sort.Strings(existing)
288+
existingStr := strings.Join(existing, ",")
289+
290+
// 2. Get desired nameservers from config
291+
desired := models.NameserversToStrings(dc.Nameservers)
292+
sort.Strings(desired)
293+
desiredStr := strings.Join(desired, ",")
294+
295+
// 3. Compare and return correction if needed
296+
if existingStr != desiredStr {
297+
return []*models.Correction{
298+
{
299+
Msg: fmt.Sprintf("Update nameservers from '%s' to '%s'", existingStr, desiredStr),
300+
F: func() error {
301+
return c.setNameservers(dc.Name, desired)
302+
},
303+
},
304+
}, nil
305+
}
306+
307+
return nil, nil
308+
}
309+
271310
// getDNSSECCorrections returns corrections that update a domain's DNSSEC state.
272311
func (c *cloudnsProvider) getDNSSECCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
273312
enabled, err := c.isDnssecEnabled(dc.Name)

0 commit comments

Comments
 (0)