dnscontrol/providers/domainnameshop/api.go
Simen Bai e9510da434
Domainnameshop provider (#1625)
* Added basic structure for domain name shop

* Finished proof of concept for domainnameshop

* Fixed handeling of IDNA for CNAME records

* Updated documentation notes

* Added docs

* Ran linter and vet

* Removed proxy config used for debugging

* Ran go generate

* Fixed issue with TTLs being restricted to a multiple of 60

* Ran tests, vet and linting and fixed flaws

* Fixed typo in docs

* Improved code based on feedback

* Fixed issues with TXT records not working properly

* Refactored according to new file layout proposed

* Updated documentation matrix

* Suggestions and corrections

* Corrected according to suggestions

Co-authored-by: Simen Bai <git@simenbai.no>
Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
2022-08-01 12:01:37 -04:00

224 lines
5.3 KiB
Go

package domainnameshop
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"golang.org/x/net/idna"
)
var rootAPIURI = "https://api.domeneshop.no/v0"
func (api *domainNameShopProvider) getDomains(domainName string) ([]domainResponse, error) {
client := &http.Client{}
req, err := http.NewRequest(http.MethodGet, rootAPIURI+"/domains?domain="+domainName, nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(api.Token, api.Secret)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var domainResp []domainResponse
err = json.NewDecoder(resp.Body).Decode(&domainResp)
if err != nil {
return nil, err
}
if domainName != "" && domainName != domainResp[0].Domain {
return nil, fmt.Errorf("invalid domain name: %q != %q", domainName, domainResp[0].Domain)
}
return domainResp, nil
}
func (api *domainNameShopProvider) getDomainID(domainName string) (string, error) {
domainResp, err := api.getDomains(domainName)
if err != nil {
return "", err
}
return strconv.Itoa(domainResp[0].ID), nil
}
func (api *domainNameShopProvider) getNS(domainName string) ([]string, error) {
domainResp, err := api.getDomains(domainName)
if err != nil {
return nil, err
}
return domainResp[0].Nameservers, nil
}
func (api *domainNameShopProvider) getDNS(domainName string) ([]domainNameShopRecord, error) {
domainID, err := api.getDomainID(domainName)
if err != nil {
return nil, err
}
client := &http.Client{}
req, err := http.NewRequest(http.MethodGet, rootAPIURI+"/domains/"+domainID+"/dns", nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(api.Token, api.Secret)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var domainResponse []domainNameShopRecord
err = json.NewDecoder(resp.Body).Decode(&domainResponse)
if err != nil {
return nil, err
}
// Post processing of the data received. Converting to correct types and setting default values.
for i := range domainResponse {
// Convert priority from string to Uint, defaulting to 0
record := &domainResponse[i]
priority, err := strconv.ParseUint(record.Priority, 10, 16)
if err != nil {
record.ActualPriority = 0
}
record.ActualPriority = uint16(priority)
// Convert port from string to Uint, defaulting to 0
port, err := strconv.ParseUint(record.Port, 10, 16)
if err != nil {
record.ActualPort = 0
}
record.ActualPort = uint16(port)
// Convert weight from string ti Uint, defaulting to 0
weight, err := strconv.ParseUint(record.Weight, 10, 16)
if err != nil {
record.ActualWeight = 0
}
record.ActualWeight = uint16(weight)
// Converting the CAA flag from string to correct value
if record.Type == "CAA" {
CaaFlag, err := strconv.ParseUint(record.ActualCAAFlag, 10, 8)
if err != nil {
record.CAAFlag = 0
}
record.CAAFlag = CaaFlag
}
// Transform data field to punycode if CNAME
if record.Type == "CNAME" {
punycodeData, err := idna.ToASCII(record.Data)
if err != nil {
return nil, err
}
record.Data = punycodeData
if !strings.HasSuffix(record.Data, ".") {
record.Data += "."
}
}
// Normalize the TTL.
record.TTL = uint16(fixTTL(uint32(record.TTL)))
// Add domain id
(&domainResponse[i]).DomainID = domainID
}
ns, err := api.getNS(domainName)
if err != nil {
return nil, err
}
// Adds NS as records
for _, nameserver := range ns {
domainResponse = append(domainResponse, domainNameShopRecord{
ID: 0,
Host: "@",
TTL: 300,
Type: "NS",
Data: nameserver + ".",
DomainID: domainID,
})
}
return domainResponse, nil
}
func (api *domainNameShopProvider) deleteRecord(domainID string, recordID string) error {
return api.sendChangeRequest(http.MethodDelete, rootAPIURI+"/domains/"+domainID+"/dns/"+recordID, nil)
}
func (api *domainNameShopProvider) CreateRecord(domainName string, dnsR *domainNameShopRecord) error {
domainID, err := api.getDomainID(domainName)
if err != nil {
return err
}
payloadBuf := new(bytes.Buffer)
err = json.NewEncoder(payloadBuf).Encode(&dnsR)
if err != nil {
return err
}
return api.sendChangeRequest(http.MethodPost, rootAPIURI+"/domains/"+domainID+"/dns", payloadBuf)
}
func (api *domainNameShopProvider) UpdateRecord(dnsR *domainNameShopRecord) error {
domainID := dnsR.DomainID
recordID := strconv.Itoa(dnsR.ID)
payloadBuf := new(bytes.Buffer)
json.NewEncoder(payloadBuf).Encode(&dnsR)
return api.sendChangeRequest(http.MethodPut, rootAPIURI+"/domains/"+domainID+"/dns/"+recordID, payloadBuf)
}
func (api *domainNameShopProvider) sendChangeRequest(method string, uri string, payload *bytes.Buffer) error {
client := &http.Client{}
var req *http.Request
var err error
if payload != nil {
req, err = http.NewRequest(method, uri, payload)
} else {
req, err = http.NewRequest(method, uri, nil)
}
if err != nil {
return err
}
req.SetBasicAuth(api.Token, api.Secret)
resp, err := client.Do(req)
if err != nil {
return err
}
switch resp.StatusCode {
case 201:
// Record is deleted
return nil
case 204:
//Update successful
return nil
case 400:
return fmt.Errorf("DNS record failed validation")
case 403:
return fmt.Errorf("not authorized")
case 404:
return fmt.Errorf("does not exist")
case 409:
return fmt.Errorf("collision")
default:
return fmt.Errorf("unknown statuscode: %v", resp.StatusCode)
}
}