dnscontrol/providers/desec/protocol.go
2020-10-26 09:25:30 -04:00

218 lines
5.8 KiB
Go

package desec
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
)
const apiBase = "https://desec.io/api/v1"
// Api layer for desec
type desecProvider struct {
domainIndex map[string]uint32
nameserversNames []string
creds struct {
tokenid string
token string
user string
password string
}
}
type domainObject struct {
Created time.Time `json:"created,omitempty"`
Keys []dnssecKey `json:"keys,omitempty"`
MinimumTTL uint32 `json:"minimum_ttl,omitempty"`
Name string `json:"name,omitempty"`
Published time.Time `json:"published,omitempty"`
}
type resourceRecord struct {
Subname string `json:"subname"`
Records []string `json:"records"`
TTL uint32 `json:"ttl,omitempty"`
Type string `json:"type"`
Target string `json:"-"`
}
type rrResponse struct {
resourceRecord
Created time.Time `json:"created"`
Domain string `json:"domain"`
Name string `json:"name"`
}
type dnssecKey struct {
Dnskey string `json:"dnskey"`
Ds []string `json:"ds"`
Flags int `json:"flags"`
Keytype string `json:"keytype"`
}
type errorResponse struct {
Detail string `json:"detail"`
}
func (c *desecProvider) fetchDomainList() error {
c.domainIndex = map[string]uint32{}
var dr []domainObject
endpoint := "/domains/"
var bodyString, err = c.get(endpoint, "GET")
if err != nil {
return fmt.Errorf("failed fetching domain list (deSEC): %s", err)
}
err = json.Unmarshal(bodyString, &dr)
if err != nil {
return err
}
for _, domain := range dr {
//We store the min ttl in the domain index
//This will be used for validation and auto correction
c.domainIndex[domain.Name] = domain.MinimumTTL
}
return nil
}
func (c *desecProvider) getRecords(domain string) ([]resourceRecord, error) {
endpoint := "/domains/%s/rrsets/"
var rrs []rrResponse
var rrsNew []resourceRecord
var bodyString, err = c.get(fmt.Sprintf(endpoint, domain), "GET")
if err != nil {
return rrsNew, fmt.Errorf("failed fetching records for domain %s (deSEC): %s", domain, err)
}
err = json.Unmarshal(bodyString, &rrs)
if err != nil {
return rrsNew, err
}
// deSEC returns round robin records as array but dnsconfig expects single entries for each record
// we will create one object per record except of TXT records which are handled as array of string by dnscontrol aswell.
for i := range rrs {
tmp := resourceRecord{
TTL: rrs[i].TTL,
Type: rrs[i].Type,
Subname: rrs[i].Subname,
Records: rrs[i].Records,
}
rrsNew = append(rrsNew, tmp)
}
return rrsNew, nil
}
func (c *desecProvider) createDomain(domain string) error {
endpoint := "/domains/"
pl := domainObject{Name: domain}
byt, _ := json.Marshal(pl)
var resp []byte
var err error
if resp, err = c.post(endpoint, "POST", byt); err != nil {
return fmt.Errorf("failed domain create (deSEC): %v", err)
}
dm := domainObject{}
err = json.Unmarshal(resp, &dm)
if err != nil {
return err
}
printer.Printf("If you want to use DNSSec please add the DS record at your registrar using one of the keys:\n")
printer.Printf("%+q", dm.Keys)
return nil
}
//upsertRR will create or override the RRSet with the provided resource record.
func (c *desecProvider) upsertRR(rr []resourceRecord, domain string) error {
endpoint := fmt.Sprintf("/domains/%s/rrsets/", domain)
byt, _ := json.Marshal(rr)
if _, err := c.post(endpoint, "PUT", byt); err != nil {
return fmt.Errorf("failed create rrset (deSEC): %v", err)
}
return nil
}
func (c *desecProvider) deleteRR(domain, shortname, t string) error {
endpoint := fmt.Sprintf("/domains/%s/rrsets/%s/%s/", domain, shortname, t)
if _, err := c.get(endpoint, "DELETE"); err != nil {
return fmt.Errorf("failed delete rrset (deSEC): %v", err)
}
return nil
}
func (c *desecProvider) get(endpoint, method string) ([]byte, error) {
retrycnt := 0
retry:
client := &http.Client{}
req, _ := http.NewRequest(method, apiBase+endpoint, nil)
q := req.URL.Query()
req.Header.Add("Authorization", fmt.Sprintf("Token %s", c.creds.token))
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
if err != nil {
return []byte{}, err
}
bodyString, _ := ioutil.ReadAll(resp.Body)
// Got error from API ?
if resp.StatusCode > 299 {
if resp.StatusCode == 429 && retrycnt < 5 {
retrycnt++
time.Sleep(500 * time.Millisecond)
goto retry
}
var errResp errorResponse
err = json.Unmarshal(bodyString, &errResp)
if err == nil {
return bodyString, fmt.Errorf("%s", errResp.Detail)
}
return bodyString, fmt.Errorf("http status %d %s, the api does not provide more information", resp.StatusCode, resp.Status)
}
return bodyString, nil
}
func (c *desecProvider) post(endpoint, method string, payload []byte) ([]byte, error) {
retrycnt := 0
retry:
client := &http.Client{}
req, err := http.NewRequest(method, apiBase+endpoint, bytes.NewReader(payload))
if err != nil {
return []byte{}, err
}
q := req.URL.Query()
if endpoint != "/auth/login/" {
req.Header.Add("Authorization", fmt.Sprintf("Token %s", c.creds.token))
}
req.Header.Set("Content-Type", "application/json")
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
if err != nil {
return []byte{}, err
}
bodyString, _ := ioutil.ReadAll(resp.Body)
// Got error from API ?
if resp.StatusCode > 299 {
if resp.StatusCode == 429 && retrycnt < 5 {
retrycnt++
time.Sleep(500 * time.Millisecond)
goto retry
}
var errResp errorResponse
err = json.Unmarshal(bodyString, &errResp)
if err == nil {
return bodyString, fmt.Errorf("http status %d %s details: %s", resp.StatusCode, resp.Status, errResp.Detail)
}
return bodyString, fmt.Errorf("http status %d %s, the api does not provide more information", resp.StatusCode, resp.Status)
}
//time.Sleep(334 * time.Millisecond)
return bodyString, nil
}