2021-03-18 11:36:42 +08:00
|
|
|
package dnsmadeeasy
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/hmac"
|
|
|
|
"crypto/sha1"
|
|
|
|
"encoding/hex"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2023-05-21 01:21:45 +08:00
|
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
2021-03-18 11:36:42 +08:00
|
|
|
"net/http"
|
|
|
|
"net/http/httputil"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
baseURLV2_0 = "https://api.dnsmadeeasy.com/V2.0/"
|
|
|
|
sandboxBaseURLV2_0 = "https://api.sandbox.dnsmadeeasy.com/V2.0/"
|
|
|
|
requestDateHeaderLayout = "Mon, 2 Jan 2006 15:04:05 MST"
|
|
|
|
initialBackoff = time.Second * 10 // First backoff delay duration
|
|
|
|
maxBackoff = time.Minute * 3 // Maximum backoff delay
|
|
|
|
)
|
|
|
|
|
|
|
|
type dnsMadeEasyRestAPI struct {
|
|
|
|
baseURL string
|
|
|
|
httpClient *http.Client
|
|
|
|
|
|
|
|
apiKey string
|
|
|
|
secretKey string
|
|
|
|
|
|
|
|
dumpHTTPRequest bool
|
|
|
|
dumpHTTPResponse bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type apiErrorResponse struct {
|
|
|
|
Error []string `json:"error"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type apiRequest struct {
|
|
|
|
method string
|
|
|
|
endpoint string
|
|
|
|
data []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
func (restApi *dnsMadeEasyRestAPI) singleDomainGet(domainID int) (*singleDomainResponse, error) {
|
|
|
|
req := &apiRequest{
|
|
|
|
method: "GET",
|
|
|
|
endpoint: fmt.Sprintf("dns/managed/%d", domainID),
|
|
|
|
}
|
|
|
|
|
|
|
|
res := &singleDomainResponse{}
|
|
|
|
_, err := restApi.sendRequest(req, &res)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (restApi *dnsMadeEasyRestAPI) multiDomainGet() (*multiDomainResponse, error) {
|
|
|
|
req := &apiRequest{
|
|
|
|
method: "GET",
|
|
|
|
endpoint: "dns/managed/",
|
|
|
|
}
|
|
|
|
|
|
|
|
res := &multiDomainResponse{}
|
|
|
|
_, err := restApi.sendRequest(req, &res)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (restApi *dnsMadeEasyRestAPI) recordGet(domainID int) (*recordResponse, error) {
|
|
|
|
req := &apiRequest{
|
|
|
|
method: "GET",
|
|
|
|
endpoint: fmt.Sprintf("dns/managed/%d/records", domainID),
|
|
|
|
}
|
|
|
|
|
|
|
|
res := &recordResponse{}
|
|
|
|
_, err := restApi.sendRequest(req, &res)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (restApi *dnsMadeEasyRestAPI) singleDomainCreate(data singleDomainRequestData) (*singleDomainResponse, error) {
|
|
|
|
jsonData, err := json.Marshal(data)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req := &apiRequest{
|
|
|
|
method: "POST",
|
2021-12-07 01:45:34 +08:00
|
|
|
endpoint: "dns/managed/",
|
2021-03-18 11:36:42 +08:00
|
|
|
data: jsonData,
|
|
|
|
}
|
|
|
|
|
|
|
|
res := &singleDomainResponse{}
|
|
|
|
_, err = restApi.sendRequest(req, &res)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (restApi *dnsMadeEasyRestAPI) multiRecordCreate(domainID int, data []recordRequestData) (*[]recordResponseDataEntry, error) {
|
|
|
|
jsonData, err := json.Marshal(data)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req := &apiRequest{
|
|
|
|
method: "POST",
|
|
|
|
endpoint: fmt.Sprintf("dns/managed/%d/records/createMulti", domainID),
|
|
|
|
data: jsonData,
|
|
|
|
}
|
|
|
|
|
|
|
|
res := &[]recordResponseDataEntry{}
|
|
|
|
_, err = restApi.sendRequest(req, &res)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (restApi *dnsMadeEasyRestAPI) multiRecordUpdate(domainID int, data []recordRequestData) error {
|
|
|
|
jsonData, err := json.Marshal(data)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
req := &apiRequest{
|
|
|
|
method: "PUT",
|
|
|
|
endpoint: fmt.Sprintf("dns/managed/%d/records/updateMulti", domainID),
|
|
|
|
data: jsonData,
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = restApi.sendRequest(req, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (restApi *dnsMadeEasyRestAPI) multiRecordDelete(domainID int, recordIDs []int) error {
|
|
|
|
params := []string{}
|
|
|
|
for i := range recordIDs {
|
|
|
|
params = append(params, fmt.Sprintf("ids=%d", recordIDs[i]))
|
|
|
|
}
|
|
|
|
|
|
|
|
req := &apiRequest{
|
|
|
|
method: "DELETE",
|
|
|
|
endpoint: fmt.Sprintf("dns/managed/%d/records?%s", domainID, strings.Join(params, "&")),
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err := restApi.sendRequest(req, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (restApi *dnsMadeEasyRestAPI) createRequestAuthHeaders() (string, string) {
|
|
|
|
t := time.Now()
|
|
|
|
requestDate := t.UTC().Format(requestDateHeaderLayout)
|
|
|
|
|
|
|
|
mac := hmac.New(sha1.New, []byte(restApi.secretKey))
|
|
|
|
mac.Write([]byte(requestDate))
|
|
|
|
macStr := hex.EncodeToString(mac.Sum(nil))
|
|
|
|
|
|
|
|
return requestDate, macStr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (restApi *dnsMadeEasyRestAPI) createRequest(request *apiRequest) (*http.Request, error) {
|
|
|
|
url := restApi.baseURL + request.endpoint
|
|
|
|
var req *http.Request
|
|
|
|
var err error
|
|
|
|
|
|
|
|
if request.method == "PUT" || request.method == "POST" {
|
|
|
|
req, err = http.NewRequest(request.method, url, bytes.NewBuffer([]byte(request.data)))
|
|
|
|
} else if request.method == "GET" || request.method == "DELETE" {
|
|
|
|
req, err = http.NewRequest(request.method, url, nil)
|
|
|
|
} else {
|
|
|
|
return nil, fmt.Errorf("unknown API request method in DNSMADEEASY REST API: %s", request.method)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
requestDate, hmac := restApi.createRequestAuthHeaders()
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
req.Header.Set("x-dnsme-apiKey", restApi.apiKey)
|
|
|
|
req.Header.Set("x-dnsme-hmac", hmac)
|
|
|
|
req.Header.Set("x-dnsme-requestDate", requestDate)
|
|
|
|
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
2023-10-31 21:22:44 +08:00
|
|
|
// DNS Made Easy only allows 150 request / 5 minutes
|
2021-03-18 11:36:42 +08:00
|
|
|
// backoff is the amount of time to sleep if a "Rate limit exceeded" error is received
|
|
|
|
// It is increased up to maxBackoff after each use
|
|
|
|
// It is reset after successful request
|
|
|
|
var backoff = initialBackoff
|
|
|
|
|
|
|
|
func (restApi *dnsMadeEasyRestAPI) sendRequest(request *apiRequest, response interface{}) (int, error) {
|
|
|
|
retry:
|
|
|
|
req, err := restApi.createRequest(request)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if restApi.dumpHTTPRequest {
|
|
|
|
dump, _ := httputil.DumpRequest(req, true)
|
2022-06-18 21:01:02 +08:00
|
|
|
printer.Printf(string(dump))
|
2021-03-18 11:36:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
res, err := restApi.httpClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
if restApi.dumpHTTPResponse {
|
|
|
|
dump, _ := httputil.DumpResponse(res, true)
|
2022-06-18 21:01:02 +08:00
|
|
|
printer.Printf(string(dump))
|
2021-03-18 11:36:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
|
|
|
|
var apiErr apiErrorResponse
|
|
|
|
err = json.NewDecoder(res.Body).Decode(&apiErr)
|
|
|
|
if err != nil {
|
|
|
|
return res.StatusCode, fmt.Errorf("DNSMADEEASY API unknown error, status code: %d", res.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(apiErr.Error) == 1 && apiErr.Error[0] == "Rate limit exceeded" {
|
2022-06-18 21:01:02 +08:00
|
|
|
printer.Printf("pausing DNSMADEEASY due to ratelimit: %v seconds\n", backoff)
|
2021-03-18 11:36:42 +08:00
|
|
|
|
|
|
|
time.Sleep(backoff)
|
|
|
|
|
|
|
|
backoff = backoff + (backoff / 2)
|
|
|
|
if backoff > maxBackoff {
|
|
|
|
backoff = maxBackoff
|
|
|
|
}
|
|
|
|
|
|
|
|
goto retry
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.StatusCode, fmt.Errorf("DNSMADEEASY API error: %s", strings.Join(apiErr.Error, " "))
|
|
|
|
}
|
|
|
|
|
|
|
|
backoff = initialBackoff
|
|
|
|
|
|
|
|
if response != nil {
|
|
|
|
err = json.NewDecoder(res.Body).Decode(&response)
|
|
|
|
if err != nil {
|
|
|
|
return res.StatusCode, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.StatusCode, nil
|
|
|
|
}
|