2020-04-28 20:40:58 +02:00
|
|
|
package desec
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
2021-07-08 16:06:54 +02:00
|
|
|
"strconv"
|
2020-04-28 20:40:58 +02:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
|
|
|
|
)
|
|
|
|
|
|
|
|
const apiBase = "https://desec.io/api/v1"
|
|
|
|
|
|
|
|
// Api layer for desec
|
2020-10-26 09:25:30 -04:00
|
|
|
type desecProvider struct {
|
2021-07-08 16:06:54 +02:00
|
|
|
domainIndex map[string]uint32 //stores the minimum ttl of each domain. (key = domain and value = ttl)
|
2020-04-28 20:40:58 +02:00
|
|
|
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"`
|
|
|
|
}
|
2021-07-08 16:06:54 +02:00
|
|
|
type nonFieldError struct {
|
|
|
|
Errors []string `json:"non_field_errors"`
|
|
|
|
}
|
2020-04-28 20:40:58 +02:00
|
|
|
|
2021-07-08 16:06:54 +02:00
|
|
|
func (c *desecProvider) authenticate() error {
|
|
|
|
endpoint := "/auth/account/"
|
|
|
|
var _, _, err = c.get(endpoint, "GET")
|
2020-04-28 20:40:58 +02:00
|
|
|
if err != nil {
|
2021-07-08 16:06:54 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *desecProvider) fetchDomain(domain string) error {
|
|
|
|
endpoint := fmt.Sprintf("/domains/%s", domain)
|
|
|
|
var dr domainObject
|
|
|
|
var bodyString, statuscode, err = c.get(endpoint, "GET")
|
|
|
|
if err != nil {
|
|
|
|
if statuscode == 404 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return fmt.Errorf("Failed fetching domain: %s", err)
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
|
|
|
err = json.Unmarshal(bodyString, &dr)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-07-08 16:06:54 +02:00
|
|
|
|
|
|
|
//deSEC allows different minimum ttls per domain
|
|
|
|
//we store the actual minimum ttl to use it in desecProvider.go GetDomainCorrections() to enforce the minimum ttl and avoid api errors.
|
|
|
|
c.domainIndex[dr.Name] = dr.MinimumTTL
|
2020-04-28 20:40:58 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-26 09:25:30 -04:00
|
|
|
func (c *desecProvider) getRecords(domain string) ([]resourceRecord, error) {
|
2020-04-28 20:40:58 +02:00
|
|
|
endpoint := "/domains/%s/rrsets/"
|
|
|
|
var rrs []rrResponse
|
2020-06-18 09:37:57 -04:00
|
|
|
var rrsNew []resourceRecord
|
2021-07-08 16:06:54 +02:00
|
|
|
var bodyString, _, err = c.get(fmt.Sprintf(endpoint, domain), "GET")
|
2020-04-28 20:40:58 +02:00
|
|
|
if err != nil {
|
2021-05-17 21:45:24 +02:00
|
|
|
return rrsNew, fmt.Errorf("Failed fetching records for domain %s (deSEC): %s", domain, err)
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
|
|
|
err = json.Unmarshal(bodyString, &rrs)
|
|
|
|
if err != nil {
|
2020-06-18 09:37:57 -04:00
|
|
|
return rrsNew, err
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
|
|
|
// 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,
|
|
|
|
}
|
2020-06-18 09:37:57 -04:00
|
|
|
rrsNew = append(rrsNew, tmp)
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
2020-06-18 09:37:57 -04:00
|
|
|
return rrsNew, nil
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
|
|
|
|
2020-10-26 09:25:30 -04:00
|
|
|
func (c *desecProvider) createDomain(domain string) error {
|
2020-04-28 20:40:58 +02:00
|
|
|
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 {
|
2021-05-17 21:45:24 +02:00
|
|
|
return fmt.Errorf("Failed domain create (deSEC): %v", err)
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
|
|
|
dm := domainObject{}
|
|
|
|
err = json.Unmarshal(resp, &dm)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-05-17 21:45:24 +02:00
|
|
|
printer.Printf("To enable DNSSEC validation for your domain, make sure to convey the DS record(s) to your registrar:\n")
|
2020-04-28 20:40:58 +02:00
|
|
|
printer.Printf("%+q", dm.Keys)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
//upsertRR will create or override the RRSet with the provided resource record.
|
2020-10-26 09:25:30 -04:00
|
|
|
func (c *desecProvider) upsertRR(rr []resourceRecord, domain string) error {
|
2020-04-28 20:40:58 +02:00
|
|
|
endpoint := fmt.Sprintf("/domains/%s/rrsets/", domain)
|
|
|
|
byt, _ := json.Marshal(rr)
|
|
|
|
if _, err := c.post(endpoint, "PUT", byt); err != nil {
|
2021-05-17 21:45:24 +02:00
|
|
|
return fmt.Errorf("Failed create RRset (deSEC): %v", err)
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-26 09:25:30 -04:00
|
|
|
func (c *desecProvider) deleteRR(domain, shortname, t string) error {
|
2020-04-28 20:40:58 +02:00
|
|
|
endpoint := fmt.Sprintf("/domains/%s/rrsets/%s/%s/", domain, shortname, t)
|
2021-07-08 16:06:54 +02:00
|
|
|
if _, _, err := c.get(endpoint, "DELETE"); err != nil {
|
2021-05-17 21:45:24 +02:00
|
|
|
return fmt.Errorf("Failed delete RRset (deSEC): %v", err)
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-07-08 16:06:54 +02:00
|
|
|
func (c *desecProvider) get(endpoint, method string) ([]byte, int, error) {
|
2020-04-28 20:40:58 +02:00
|
|
|
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 {
|
2021-07-08 16:06:54 +02:00
|
|
|
return []byte{}, 0, err
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
bodyString, _ := ioutil.ReadAll(resp.Body)
|
|
|
|
// Got error from API ?
|
|
|
|
if resp.StatusCode > 299 {
|
|
|
|
if resp.StatusCode == 429 && retrycnt < 5 {
|
|
|
|
retrycnt++
|
2021-07-08 16:06:54 +02:00
|
|
|
//we've got rate limiting and will try to get the Retry-After Header if this fails we fallback to sleep for 500ms max. 5 retries.
|
|
|
|
waitfor := resp.Header.Get("Retry-After")
|
|
|
|
if waitfor != "" {
|
|
|
|
wait, err := strconv.ParseInt(waitfor, 10, 64)
|
|
|
|
if err == nil {
|
|
|
|
if wait > 180 {
|
|
|
|
return []byte{}, 0, fmt.Errorf("rate limiting exceeded")
|
|
|
|
}
|
|
|
|
printer.Warnf("Rate limiting.. waiting for %s seconds", waitfor)
|
|
|
|
time.Sleep(time.Duration(wait+1) * time.Second)
|
|
|
|
goto retry
|
|
|
|
}
|
|
|
|
}
|
|
|
|
printer.Warnf("Rate limiting.. waiting for 500 milliseconds")
|
2020-04-28 20:40:58 +02:00
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
goto retry
|
|
|
|
}
|
|
|
|
var errResp errorResponse
|
2021-07-08 16:06:54 +02:00
|
|
|
var nfieldErrors []nonFieldError
|
2020-04-28 20:40:58 +02:00
|
|
|
err = json.Unmarshal(bodyString, &errResp)
|
|
|
|
if err == nil {
|
2021-07-08 16:06:54 +02:00
|
|
|
return bodyString, resp.StatusCode, fmt.Errorf("%s", errResp.Detail)
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
2021-07-08 16:06:54 +02:00
|
|
|
err = json.Unmarshal(bodyString, &nfieldErrors)
|
|
|
|
if err == nil && len(nfieldErrors) > 0 {
|
|
|
|
if len(nfieldErrors[0].Errors) > 0 {
|
|
|
|
return bodyString, resp.StatusCode, fmt.Errorf("%s", nfieldErrors[0].Errors[0])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return bodyString, resp.StatusCode, fmt.Errorf("HTTP status %s Body: %s, the API does not provide more information", resp.Status, bodyString)
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
2021-07-08 16:06:54 +02:00
|
|
|
return bodyString, resp.StatusCode, nil
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
|
|
|
|
2020-10-26 09:25:30 -04:00
|
|
|
func (c *desecProvider) post(endpoint, method string, payload []byte) ([]byte, error) {
|
2020-04-28 20:40:58 +02:00
|
|
|
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++
|
2021-07-08 16:06:54 +02:00
|
|
|
//we've got rate limiting and will try to get the Retry-After Header if this fails we fallback to sleep for 500ms max. 5 retries.
|
|
|
|
waitfor := resp.Header.Get("Retry-After")
|
|
|
|
if waitfor != "" {
|
|
|
|
wait, err := strconv.ParseInt(waitfor, 10, 64)
|
|
|
|
if err == nil {
|
|
|
|
if wait > 180 {
|
|
|
|
return []byte{}, fmt.Errorf("rate limiting exceeded")
|
|
|
|
}
|
|
|
|
printer.Warnf("Rate limiting.. waiting for %s seconds", waitfor)
|
|
|
|
time.Sleep(time.Duration(wait+1) * time.Second)
|
|
|
|
goto retry
|
|
|
|
}
|
|
|
|
}
|
|
|
|
printer.Warnf("Rate limiting.. waiting for 500 milliseconds")
|
2020-04-28 20:40:58 +02:00
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
goto retry
|
|
|
|
}
|
|
|
|
var errResp errorResponse
|
2021-07-08 16:06:54 +02:00
|
|
|
var nfieldErrors []nonFieldError
|
2020-04-28 20:40:58 +02:00
|
|
|
err = json.Unmarshal(bodyString, &errResp)
|
|
|
|
if err == nil {
|
2021-05-17 21:45:24 +02:00
|
|
|
return bodyString, fmt.Errorf("HTTP status %d %s details: %s", resp.StatusCode, resp.Status, errResp.Detail)
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
2021-07-08 16:06:54 +02:00
|
|
|
err = json.Unmarshal(bodyString, &nfieldErrors)
|
|
|
|
if err == nil && len(nfieldErrors) > 0 {
|
|
|
|
if len(nfieldErrors[0].Errors) > 0 {
|
|
|
|
return bodyString, fmt.Errorf("%s", nfieldErrors[0].Errors[0])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return bodyString, fmt.Errorf("HTTP status %s Body: %s, the API does not provide more information", resp.Status, bodyString)
|
2020-04-28 20:40:58 +02:00
|
|
|
}
|
|
|
|
//time.Sleep(334 * time.Millisecond)
|
|
|
|
return bodyString, nil
|
|
|
|
}
|