2020-05-06 23:27:35 +00:00
|
|
|
package internal
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"path"
|
|
|
|
)
|
|
|
|
|
|
|
|
const baseURL = "https://desec.io/api/v1/"
|
|
|
|
|
|
|
|
// Client deSec API client.
|
|
|
|
type Client struct {
|
|
|
|
HTTPClient *http.Client
|
|
|
|
BaseURL string
|
|
|
|
|
|
|
|
token string
|
|
|
|
}
|
|
|
|
|
2020-05-08 17:35:25 +00:00
|
|
|
// NewClient creates a new Client.
|
2020-05-06 23:27:35 +00:00
|
|
|
func NewClient(token string) *Client {
|
|
|
|
return &Client{
|
|
|
|
HTTPClient: http.DefaultClient,
|
|
|
|
BaseURL: baseURL,
|
|
|
|
token: token,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetTxtRRSet gets a RRSet.
|
|
|
|
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#retrieving-a-specific-rrset
|
|
|
|
func (c *Client) GetTxtRRSet(domainName string, subName string) (*RRSet, error) {
|
|
|
|
if subName == "" {
|
|
|
|
subName = "@"
|
|
|
|
}
|
|
|
|
|
|
|
|
endpoint, err := c.createEndpoint("domains", domainName, "rrsets", subName, "TXT")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create endpoint: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.token))
|
|
|
|
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to call API: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
|
|
var notFound NotFound
|
|
|
|
err = json.Unmarshal(body, ¬Found)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error: %d: %s", resp.StatusCode, string(body))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, ¬Found
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
return nil, fmt.Errorf("error: %d: %s", resp.StatusCode, string(body))
|
|
|
|
}
|
|
|
|
|
|
|
|
var rrSet RRSet
|
|
|
|
err = json.Unmarshal(body, &rrSet)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to umarshal response body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &rrSet, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddTxtRRSet creates a new RRSet.
|
|
|
|
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#creating-a-tlsa-rrset
|
|
|
|
func (c *Client) AddTxtRRSet(rrSet RRSet) (*RRSet, error) {
|
|
|
|
endpoint, err := c.createEndpoint("domains", rrSet.Domain, "rrsets")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create endpoint: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
raw, err := json.Marshal(rrSet)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(raw))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.token))
|
|
|
|
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to call API: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
|
|
return nil, fmt.Errorf("error: %d: %s", resp.StatusCode, string(body))
|
|
|
|
}
|
|
|
|
|
|
|
|
var newRRSet RRSet
|
|
|
|
err = json.Unmarshal(body, &newRRSet)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to umarshal response body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &newRRSet, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateTxtRRSet updates RRSet records.
|
|
|
|
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#modifying-an-rrset
|
|
|
|
func (c *Client) UpdateTxtRRSet(domainName string, subName string, records []string) (*RRSet, error) {
|
|
|
|
if subName == "" {
|
|
|
|
subName = "@"
|
|
|
|
}
|
|
|
|
|
|
|
|
endpoint, err := c.createEndpoint("domains", domainName, "rrsets", subName, "TXT")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create endpoint: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
raw, err := json.Marshal(RRSet{Records: records})
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewReader(raw))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.token))
|
|
|
|
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to call API: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// when a RRSet is deleted (empty records)
|
|
|
|
if resp.StatusCode == http.StatusNoContent {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
return nil, fmt.Errorf("error: %d: %s", resp.StatusCode, string(body))
|
|
|
|
}
|
|
|
|
|
|
|
|
var updatedRRSet RRSet
|
|
|
|
err = json.Unmarshal(body, &updatedRRSet)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to umarshal response body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &updatedRRSet, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteTxtRRSet deletes a RRset.
|
|
|
|
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#deleting-an-rrset
|
|
|
|
func (c *Client) DeleteTxtRRSet(domainName string, subName string) error {
|
|
|
|
if subName == "" {
|
|
|
|
subName = "@"
|
|
|
|
}
|
|
|
|
|
|
|
|
endpoint, err := c.createEndpoint("domains", domainName, "rrsets", subName, "TXT")
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to create endpoint: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodDelete, endpoint, nil)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.token))
|
|
|
|
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to call API: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to read response body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusNoContent {
|
|
|
|
return fmt.Errorf("error: %d: %s", resp.StatusCode, string(body))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) createEndpoint(parts ...string) (string, error) {
|
|
|
|
base, err := url.Parse(c.BaseURL)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
endpoint, err := base.Parse(path.Join(base.Path, path.Join(parts...)))
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
endpoint.Path += "/"
|
|
|
|
|
|
|
|
return endpoint.String(), nil
|
|
|
|
}
|