1package autodns
2
3import (
4	"bytes"
5	"encoding/json"
6	"fmt"
7	"io"
8	"net/http"
9	"path"
10	"strconv"
11)
12
13const (
14	defaultEndpoint = "https://api.autodns.com/v1/"
15)
16
17type ResponseMessage struct {
18	Text     string   `json:"text"`
19	Messages []string `json:"messages"`
20	Objects  []string `json:"objects"`
21	Code     string   `json:"code"`
22	Status   string   `json:"status"`
23}
24
25type ResponseStatus struct {
26	Code string `json:"code"`
27	Text string `json:"text"`
28	Type string `json:"type"`
29}
30
31type ResponseObject struct {
32	Type    string `json:"type"`
33	Value   string `json:"value"`
34	Summary int32  `json:"summary"`
35	Data    string
36}
37
38type DataZoneResponse struct {
39	STID     string             `json:"stid"`
40	CTID     string             `json:"ctid"`
41	Messages []*ResponseMessage `json:"messages"`
42	Status   *ResponseStatus    `json:"status"`
43	Object   interface{}        `json:"object"`
44	Data     []*Zone            `json:"data"`
45}
46
47// ResourceRecord holds a resource record.
48type ResourceRecord struct {
49	Name  string `json:"name"`
50	TTL   int64  `json:"ttl"`
51	Type  string `json:"type"`
52	Value string `json:"value"`
53	Pref  int32  `json:"pref,omitempty"`
54}
55
56// Zone is an autodns zone record with all for us relevant fields.
57type Zone struct {
58	Name              string            `json:"origin"`
59	ResourceRecords   []*ResourceRecord `json:"resourceRecords"`
60	Action            string            `json:"action"`
61	VirtualNameServer string            `json:"virtualNameServer"`
62}
63
64type ZoneStream struct {
65	Adds    []*ResourceRecord `json:"adds"`
66	Removes []*ResourceRecord `json:"rems"`
67}
68
69func (d *DNSProvider) addTxtRecord(domain string, records []*ResourceRecord) (*Zone, error) {
70	zoneStream := &ZoneStream{Adds: records}
71
72	return d.makeZoneUpdateRequest(zoneStream, domain)
73}
74
75func (d *DNSProvider) removeTXTRecord(domain string, records []*ResourceRecord) error {
76	zoneStream := &ZoneStream{Removes: records}
77
78	_, err := d.makeZoneUpdateRequest(zoneStream, domain)
79	return err
80}
81
82func (d *DNSProvider) makeZoneUpdateRequest(zoneStream *ZoneStream, domain string) (*Zone, error) {
83	reqBody := &bytes.Buffer{}
84	if err := json.NewEncoder(reqBody).Encode(zoneStream); err != nil {
85		return nil, err
86	}
87
88	req, err := d.makeRequest(http.MethodPost, path.Join("zone", domain, "_stream"), reqBody)
89	if err != nil {
90		return nil, err
91	}
92
93	var resp *Zone
94	if err := d.sendRequest(req, &resp); err != nil {
95		return nil, err
96	}
97	return resp, nil
98}
99
100func (d *DNSProvider) makeRequest(method, resource string, body io.Reader) (*http.Request, error) {
101	uri, err := d.config.Endpoint.Parse(resource)
102	if err != nil {
103		return nil, err
104	}
105
106	req, err := http.NewRequest(method, uri.String(), body)
107	if err != nil {
108		return nil, err
109	}
110
111	req.Header.Set("Content-Type", "application/json")
112	req.Header.Set("X-Domainrobot-Context", strconv.Itoa(d.config.Context))
113	req.SetBasicAuth(d.config.Username, d.config.Password)
114
115	return req, nil
116}
117
118func (d *DNSProvider) sendRequest(req *http.Request, result interface{}) error {
119	resp, err := d.config.HTTPClient.Do(req)
120	if err != nil {
121		return err
122	}
123
124	if err = checkResponse(resp); err != nil {
125		return err
126	}
127
128	defer func() { _ = resp.Body.Close() }()
129
130	if result == nil {
131		return nil
132	}
133
134	raw, err := io.ReadAll(resp.Body)
135	if err != nil {
136		return err
137	}
138
139	err = json.Unmarshal(raw, result)
140	if err != nil {
141		return fmt.Errorf("unmarshaling %T error [status code=%d]: %w: %s", result, resp.StatusCode, err, string(raw))
142	}
143	return err
144}
145
146func checkResponse(resp *http.Response) error {
147	if resp.StatusCode < http.StatusBadRequest {
148		return nil
149	}
150
151	if resp.Body == nil {
152		return fmt.Errorf("response body is nil, status code=%d", resp.StatusCode)
153	}
154
155	defer func() { _ = resp.Body.Close() }()
156
157	raw, err := io.ReadAll(resp.Body)
158	if err != nil {
159		return fmt.Errorf("unable to read body: status code=%d, error=%w", resp.StatusCode, err)
160	}
161
162	return fmt.Errorf("status code=%d: %s", resp.StatusCode, string(raw))
163}
164