github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/cloudns/cloudnsProvider.go (about) 1 package cloudns 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strconv" 7 8 "github.com/miekg/dns/dnsutil" 9 10 "github.com/StackExchange/dnscontrol/v2/models" 11 "github.com/StackExchange/dnscontrol/v2/providers" 12 "github.com/StackExchange/dnscontrol/v2/providers/diff" 13 ) 14 15 /* 16 17 CloDNS API DNS provider: 18 19 Info required in `creds.json`: 20 - auth-id 21 - auth-password 22 23 */ 24 25 // NewCloudns creates the provider. 26 func NewCloudns(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 27 c := &api{} 28 29 c.creds.id, c.creds.password = m["auth-id"], m["auth-password"] 30 if c.creds.id == "" || c.creds.password == "" { 31 return nil, fmt.Errorf("missing ClouDNS auth-id and auth-password") 32 } 33 34 // Get a domain to validate authentication 35 if err := c.fetchDomainList(); err != nil { 36 return nil, err 37 } 38 39 return c, nil 40 } 41 42 var features = providers.DocumentationNotes{ 43 providers.DocDualHost: providers.Unimplemented(), 44 providers.DocOfficiallySupported: providers.Cannot(), 45 providers.DocCreateDomains: providers.Can(), 46 providers.CanUseAlias: providers.Can(), 47 providers.CanUseSRV: providers.Can(), 48 providers.CanUseSSHFP: providers.Can(), 49 providers.CanUseCAA: providers.Can(), 50 providers.CanUseTLSA: providers.Can(), 51 providers.CanUsePTR: providers.Unimplemented(), 52 providers.CanGetZones: providers.Unimplemented(), 53 } 54 55 func init() { 56 providers.RegisterDomainServiceProviderType("CLOUDNS", NewCloudns, features) 57 } 58 59 // GetNameservers returns the nameservers for a domain. 60 func (c *api) GetNameservers(domain string) ([]*models.Nameserver, error) { 61 if len(c.nameserversNames) == 0 { 62 c.fetchAvailableNameservers() 63 } 64 return models.StringsToNameservers(c.nameserversNames), nil 65 } 66 67 // GetDomainCorrections returns the corrections for a domain. 68 func (c *api) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 69 dc, err := dc.Copy() 70 if err != nil { 71 return nil, err 72 } 73 74 dc.Punycode() 75 76 if c.domainIndex == nil { 77 if err := c.fetchDomainList(); err != nil { 78 return nil, err 79 } 80 } 81 domainID, ok := c.domainIndex[dc.Name] 82 if !ok { 83 return nil, fmt.Errorf("'%s' not a zone in ClouDNS account", dc.Name) 84 } 85 86 records, err := c.getRecords(domainID) 87 if err != nil { 88 return nil, err 89 } 90 91 existingRecords := make([]*models.RecordConfig, len(records), len(records)+len(c.nameserversNames)) 92 for i := range records { 93 existingRecords[i] = toRc(dc, &records[i]) 94 } 95 // Normalize 96 models.PostProcessRecords(existingRecords) 97 98 // ClouDNS doesn't allow selecting an arbitrary TTL, only a set of predefined values https://asia.cloudns.net/wiki/article/188/ 99 // We need to make sure we don't change it every time if it is as close as it's going to get 100 for _, record := range dc.Records { 101 record.TTL = fixTTL(record.TTL) 102 } 103 104 differ := diff.New(dc) 105 _, create, del, modify := differ.IncrementalDiff(existingRecords) 106 107 var corrections []*models.Correction 108 109 // Deletes first so changing type works etc. 110 for _, m := range del { 111 id := m.Existing.Original.(*domainRecord).ID 112 corr := &models.Correction{ 113 Msg: fmt.Sprintf("%s, ClouDNS ID: %s", m.String(), id), 114 F: func() error { 115 return c.deleteRecord(domainID, id) 116 }, 117 } 118 corrections = append(corrections, corr) 119 } 120 121 for _, m := range create { 122 req, err := toReq(m.Desired) 123 if err != nil { 124 return nil, err 125 } 126 127 corr := &models.Correction{ 128 Msg: m.String(), 129 F: func() error { 130 return c.createRecord(domainID, req) 131 }, 132 } 133 corrections = append(corrections, corr) 134 } 135 for _, m := range modify { 136 id := m.Existing.Original.(*domainRecord).ID 137 req, err := toReq(m.Desired) 138 if err != nil { 139 return nil, err 140 } 141 142 corr := &models.Correction{ 143 Msg: fmt.Sprintf("%s, ClouDNS ID: %s: ", m.String(), id), 144 F: func() error { 145 return c.modifyRecord(domainID, id, req) 146 }, 147 } 148 corrections = append(corrections, corr) 149 } 150 151 return corrections, nil 152 } 153 154 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. 155 func (client *api) GetZoneRecords(domain string) (models.Records, error) { 156 return nil, fmt.Errorf("not implemented") 157 // This enables the get-zones subcommand. 158 // Implement this by extracting the code from GetDomainCorrections into 159 // a single function. For most providers this should be relatively easy. 160 } 161 162 // EnsureDomainExists returns an error if domain doesn't exist. 163 func (c *api) EnsureDomainExists(domain string) error { 164 if err := c.fetchDomainList(); err != nil { 165 return err 166 } 167 // domain already exists 168 if _, ok := c.domainIndex[domain]; ok { 169 return nil 170 } 171 return c.createDomain(domain) 172 } 173 174 func toRc(dc *models.DomainConfig, r *domainRecord) *models.RecordConfig { 175 176 ttl, _ := strconv.ParseUint(r.TTL, 10, 32) 177 priority, _ := strconv.ParseUint(r.Priority, 10, 32) 178 weight, _ := strconv.ParseUint(r.Weight, 10, 32) 179 port, _ := strconv.ParseUint(r.Port, 10, 32) 180 181 rc := &models.RecordConfig{ 182 Type: r.Type, 183 TTL: uint32(ttl), 184 MxPreference: uint16(priority), 185 SrvPriority: uint16(priority), 186 SrvWeight: uint16(weight), 187 SrvPort: uint16(port), 188 Original: r, 189 } 190 rc.SetLabel(r.Host, dc.Name) 191 192 switch rtype := r.Type; rtype { // #rtype_variations 193 case "TXT": 194 rc.SetTargetTXT(r.Target) 195 case "CNAME", "MX", "NS", "SRV", "ALIAS": 196 rc.SetTarget(dnsutil.AddOrigin(r.Target+".", dc.Name)) 197 case "CAA": 198 caaFlag, _ := strconv.ParseUint(r.CaaFlag, 10, 32) 199 rc.CaaFlag = uint8(caaFlag) 200 rc.CaaTag = r.CaaTag 201 rc.SetTarget(r.CaaValue) 202 case "TLSA": 203 tlsaUsage, _ := strconv.ParseUint(r.TlsaUsage, 10, 32) 204 rc.TlsaUsage = uint8(tlsaUsage) 205 tlsaSelector, _ := strconv.ParseUint(r.TlsaSelector, 10, 32) 206 rc.TlsaSelector = uint8(tlsaSelector) 207 tlsaMatchingType, _ := strconv.ParseUint(r.TlsaMatchingType, 10, 32) 208 rc.TlsaMatchingType = uint8(tlsaMatchingType) 209 rc.SetTarget(r.Target) 210 case "SSHFP": 211 sshfpAlgorithm, _ := strconv.ParseUint(r.SshfpAlgorithm, 10, 32) 212 rc.SshfpAlgorithm = uint8(sshfpAlgorithm) 213 sshfpFingerprint, _ := strconv.ParseUint(r.SshfpFingerprint, 10, 32) 214 rc.SshfpFingerprint = uint8(sshfpFingerprint) 215 rc.SetTarget(r.Target) 216 default: 217 rc.SetTarget(r.Target) 218 } 219 220 return rc 221 } 222 223 func toReq(rc *models.RecordConfig) (requestParams, error) { 224 req := requestParams{ 225 "record-type": rc.Type, 226 "host": rc.GetLabel(), 227 "record": rc.GetTargetField(), 228 "ttl": strconv.Itoa(int(rc.TTL)), 229 } 230 231 // ClouDNS doesn't use "@", it uses an empty name 232 if req["host"] == "@" { 233 req["host"] = "" 234 } 235 236 switch rc.Type { // #rtype_variations 237 case "A", "AAAA", "NS", "PTR", "TXT", "SOA", "ALIAS", "CNAME": 238 // Nothing special. 239 case "MX": 240 req["priority"] = strconv.Itoa(int(rc.MxPreference)) 241 case "SRV": 242 req["priority"] = strconv.Itoa(int(rc.SrvPriority)) 243 req["weight"] = strconv.Itoa(int(rc.SrvWeight)) 244 req["port"] = strconv.Itoa(int(rc.SrvPort)) 245 case "CAA": 246 req["caa_flag"] = strconv.Itoa(int(rc.CaaFlag)) 247 req["caa_type"] = rc.CaaTag 248 req["caa_value"] = rc.Target 249 case "TLSA": 250 req["tlsa_usage"] = strconv.Itoa(int(rc.TlsaUsage)) 251 req["tlsa_selector"] = strconv.Itoa(int(rc.TlsaSelector)) 252 req["tlsa_matching_type"] = strconv.Itoa(int(rc.TlsaMatchingType)) 253 case "SSHFP": 254 req["algorithm"] = strconv.Itoa(int(rc.SshfpAlgorithm)) 255 req["fptype"] = strconv.Itoa(int(rc.SshfpFingerprint)) 256 default: 257 msg := fmt.Sprintf("ClouDNS.toReq rtype %v unimplemented", rc.Type) 258 panic(msg) 259 // We panic so that we quickly find any switch statements 260 } 261 262 return req, nil 263 }