dnscontrol/providers/cloudns/api.go
Tom Limoncelli 60470a3886
BUG: Some DNS zones are downloaded twice (#2120)
Signed-off-by: Amelia Aronsohn <squirrel@wearing.black>
Co-authored-by: Tom Limoncelli <tal@whatexit.org>
Co-authored-by: Grégoire Henry <hnrgrgr@users.noreply.github.com>
Co-authored-by: Amelia Aronsohn <squirrel@wearing.black>
Co-authored-by: Kai Schwarz <kschwarz@hexonet.net>
Co-authored-by: Asif Nawaz <asif.nawaz@centralnic.com>
Co-authored-by: imlonghao <git@imlonghao.com>
Co-authored-by: Will Power <1619102+willpower232@users.noreply.github.com>
2023-04-14 15:22:23 -04:00

252 lines
6.6 KiB
Go

package cloudns
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
// Api layer for ClouDNS
type cloudnsProvider struct {
domainIndex map[string]string
nameserversNames []string
creds struct {
id string
password string
subid string
}
}
type requestParams map[string]string
type errorResponse struct {
Status string `json:"status"`
Description string `json:"statusDescription"`
}
type nameserverRecord struct {
Type string `json:"type"`
Name string `json:"name"`
}
type nameserverResponse []nameserverRecord
type zoneRecord struct {
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
Zone string `json:"zone"`
}
type zoneResponse []zoneRecord
type domainRecord struct {
ID string `json:"id"`
Type string `json:"type"`
Host string `json:"host"`
Target string `json:"record"`
Priority string `json:"priority"`
Weight string `json:"weight"`
Port string `json:"port"`
Service string `json:"service"`
Protocol string `json:"protocol"`
TTL string `json:"ttl"`
Status int8 `json:"status"`
CaaFlag string `json:"caa_flag,omitempty"`
CaaTag string `json:"caa_type,omitempty"`
CaaValue string `json:"caa_value,omitempty"`
TlsaUsage string `json:"tlsa_usage,omitempty"`
TlsaSelector string `json:"tlsa_selector,omitempty"`
TlsaMatchingType string `json:"tlsa_matching_type,omitempty"`
SshfpAlgorithm string `json:"algorithm,omitempty"`
SshfpFingerprint string `json:"fp_type,omitempty"`
DsKeyTag string `json:"key_tag,omitempty"`
DsAlgorithm string `json:"dsalgorithm,omitempty"`
DsDigestType string `json:"digest_type,omitempty"`
DsDigest string `json:"dsdigest,omitempty"`
}
type recordResponse map[string]domainRecord
var allowedTTLValues = []uint32{}
func (c *cloudnsProvider) fetchAvailableNameservers() error {
c.nameserversNames = nil
var bodyString, err = c.get("/dns/available-name-servers.json", requestParams{})
if err != nil {
return fmt.Errorf("failed fetching available nameservers list from ClouDNS: %s", err)
}
var nr nameserverResponse
json.Unmarshal(bodyString, &nr)
for _, nameserver := range nr {
if nameserver.Type == "premium" {
c.nameserversNames = append(c.nameserversNames, nameserver.Name)
}
}
return nil
}
func (c *cloudnsProvider) fetchAvailableTTLValues(domain string) error {
allowedTTLValues = nil
params := requestParams{
"domain-name": domain,
}
var bodyString, err = c.get("/dns/get-available-ttl.json", params)
if err != nil {
return fmt.Errorf("failed fetching available TTL values list from ClouDNS: %s", err)
}
json.Unmarshal(bodyString, &allowedTTLValues)
return nil
}
func (c *cloudnsProvider) fetchDomainList() error {
// FIXME(tlim): This should return nil if c.domainIndex != nil. Then all
// the callers won't have to check if c.domainIndex == nil before calling.
c.domainIndex = map[string]string{}
rowsPerPage := 100
page := 1
for {
var dr zoneResponse
params := requestParams{
"page": strconv.Itoa(page),
"rows-per-page": strconv.Itoa(rowsPerPage),
}
endpoint := "/dns/list-zones.json"
var bodyString, err = c.get(endpoint, params)
if err != nil {
return fmt.Errorf("failed fetching domain list from ClouDNS: %s", err)
}
json.Unmarshal(bodyString, &dr)
for _, domain := range dr {
c.domainIndex[domain.Name] = domain.Name
}
if len(dr) < rowsPerPage {
break
}
page++
}
return nil
}
func (c *cloudnsProvider) createDomain(domain string) error {
params := requestParams{
"domain-name": domain,
"zone-type": "master",
}
if _, err := c.get("/dns/register.json", params); err != nil {
return fmt.Errorf("failed create domain (ClouDNS): %s", err)
}
return nil
}
func (c *cloudnsProvider) createRecord(domainID string, rec requestParams) error {
rec["domain-name"] = domainID
if _, err := c.get("/dns/add-record.json", rec); err != nil { // here we add record
return fmt.Errorf("failed create record (ClouDNS): %s", err)
}
return nil
}
func (c *cloudnsProvider) deleteRecord(domainID string, recordID string) error {
params := requestParams{
"domain-name": domainID,
"record-id": recordID,
}
if _, err := c.get("/dns/delete-record.json", params); err != nil {
return fmt.Errorf("failed delete record (ClouDNS): %s", err)
}
return nil
}
func (c *cloudnsProvider) modifyRecord(domainID string, recordID string, rec requestParams) error {
rec["domain-name"] = domainID
rec["record-id"] = recordID
if _, err := c.get("/dns/mod-record.json", rec); err != nil {
return fmt.Errorf("failed update (ClouDNS): %s", err)
}
return nil
}
func (c *cloudnsProvider) getRecords(id string) ([]domainRecord, error) {
params := requestParams{"domain-name": id}
var bodyString, err = c.get("/dns/records.json", params)
if err != nil {
return nil, fmt.Errorf("failed fetching record list from ClouDNS: %s", err)
}
var dr recordResponse
json.Unmarshal(bodyString, &dr)
var records []domainRecord
for _, rec := range dr {
records = append(records, rec)
}
return records, nil
}
func (c *cloudnsProvider) get(endpoint string, params requestParams) ([]byte, error) {
client := &http.Client{}
req, _ := http.NewRequest("GET", "https://api.cloudns.net"+endpoint, nil)
q := req.URL.Query()
//TODO: Support sub-auth-user https://asia.cloudns.net/wiki/article/42/
// Add auth params
q.Add("auth-id", c.creds.id)
q.Add("auth-password", c.creds.password)
q.Add("sub-auth-id", c.creds.subid)
for pName, pValue := range params {
q.Add(pName, pValue)
}
req.URL.RawQuery = q.Encode()
// ClouDNS has a rate limit (not documented) of 10 request/second
// so we do a very primitive rate-limiting here - delay every request for 100ms - so max. 10 requests/second ...
time.Sleep(100 * time.Millisecond)
resp, err := client.Do(req)
if err != nil {
return []byte{}, err
}
bodyString, _ := io.ReadAll(resp.Body)
// Got error from API ?
var errResp errorResponse
err = json.Unmarshal(bodyString, &errResp)
if err == nil {
if errResp.Status == "Failed" {
return bodyString, fmt.Errorf("ClouDNS API error: %s URL:%s%s ", errResp.Description, req.Host, req.URL.RequestURI())
}
}
return bodyString, nil
}
func fixTTL(ttl uint32) uint32 {
// if the TTL is larger than the largest allowed value, return the largest allowed value
if ttl > allowedTTLValues[len(allowedTTLValues)-1] {
return allowedTTLValues[len(allowedTTLValues)-1]
}
for _, v := range allowedTTLValues {
if v >= ttl {
return v
}
}
return allowedTTLValues[0]
}