blob: 76a0d0c120a7b5a125756cd349833c6e0634c0fa [file] [log] [blame]
giolekvaeea069c2021-07-19 18:13:08 +04001package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "io/ioutil"
8 "net/http"
9 "net/http/httputil"
10 "time"
11)
12
13const (
14 GandiLiveDnsBaseUrl = "https://dns.api.gandi.net/api/v5"
15)
16
17type GandiClient struct {
18 apiKey string
19 dumpRequestResponse bool
20}
21
22type GandiRRSet struct {
23 Type string `json:"rrset_type"`
24 TTL int `json:"rrset_ttl"`
25 Name string `json:"rrset_name"`
26 Values []string `json:"rrset_values"`
27}
28
29type GandiRRSetValues struct {
30 TTL int `json:"rrset_ttl"`
31 Values []string `json:"rrset_values"`
32}
33
34func NewGandiClient(apiKey string) *GandiClient {
35 return &GandiClient{
36 apiKey: apiKey,
37 dumpRequestResponse: false,
38 }
39}
40
41func (c *GandiClient) gandiRecordsUrl(domain string) string {
42 return fmt.Sprintf("%s/domains/%s/records", GandiLiveDnsBaseUrl, domain)
43}
44
45func (c *GandiClient) doRequest(req *http.Request, readResponseBody bool) (int, []byte, error) {
46 if c.dumpRequestResponse {
47 dump, _ := httputil.DumpRequest(req, true)
48 fmt.Printf("Request: %q\n", dump)
49 }
50
51 req.Header.Set("X-Api-Key", c.apiKey)
52
53 client := http.Client{
54 Timeout: 30 * time.Second,
55 }
56
57 res, err := client.Do(req)
58 if err != nil {
59 return 0, nil, err
60 }
61
62 if c.dumpRequestResponse {
63 dump, _ := httputil.DumpResponse(res, true)
64 fmt.Printf("Response: %q\n", dump)
65 }
66
67 if res.StatusCode == http.StatusOK && readResponseBody {
68 data, err := ioutil.ReadAll(res.Body)
69 if err != nil {
70 return 0, nil, err
71 }
72 return res.StatusCode, data, nil
73 }
74
75 return res.StatusCode, nil, nil
76}
77
78func (c *GandiClient) HasTxtRecord(domain *string, name *string) (bool, error) {
79 // curl -H "X-Api-Key: $APIKEY" \
80 // https://dns.api.gandi.net/api/v5/domains/<DOMAIN>/records/<NAME>/<TYPE>
81 url := fmt.Sprintf("%s/%s/TXT", c.gandiRecordsUrl(*domain), *name)
82 req, err := http.NewRequest("GET", url, nil)
83 if err != nil {
84 return false, err
85 }
86
87 status, _, err := c.doRequest(req, false)
88 if err != nil {
89 return false, err
90 }
91
92 if status == http.StatusNotFound {
93 return false, nil
94 } else if status == http.StatusOK {
95 // Maybe parse response body here to really ensure that the record is present
96 return true, nil
97 } else {
98 return false, fmt.Errorf("unexpected HTTP status: %d", status)
99 }
100}
101
102func (c *GandiClient) CreateTxtRecord(domain *string, name *string, value *string, ttl int) error {
103 // curl -X POST -H "Content-Type: application/json" \
104 // -H "X-Api-Key: $APIKEY" \
105 // -d '{"rrset_name": "<NAME>",
106 // "rrset_type": "<TYPE>",
107 // "rrset_ttl": 10800,
108 // "rrset_values": ["<VALUE>"]}' \
109 // https://dns.api.gandi.net/api/v5/domains/<DOMAIN>/records
110 rrs := GandiRRSet{Name: *name, Type: "TXT", TTL: ttl, Values: []string{*value}}
111 body, err := json.Marshal(rrs)
112 if err != nil {
113 return fmt.Errorf("cannot marshall to json: %v", err)
114 }
115
116 url := c.gandiRecordsUrl(*domain)
117 req, err := http.NewRequest("POST", url, bytes.NewReader(body))
118 if err != nil {
119 return err
120 }
121
122 req.Header.Set("Content-Type", "application/json")
123
124 status, _, err := c.doRequest(req, false)
125 if err != nil {
126 return err
127 }
128
129 if status != http.StatusCreated && status != http.StatusOK {
130 return fmt.Errorf("failed creating TXT record: %v", err)
131 }
132
133 return nil
134}
135
136func (c *GandiClient) UpdateTxtRecord(domain *string, name *string, value *string, ttl int) error {
137 // curl -X PUT -H "Content-Type: application/json" \
138 // -H "X-Api-Key: $APIKEY" \
139 // -d '{"rrset_ttl": 10800,
140 // "rrset_values":["<VALUE>"]}' \
141 // https://dns.api.gandi.net/api/v5/domains/<DOMAIN>/records/<NAME>/<TYPE>
142 rrs := GandiRRSetValues{TTL: ttl, Values: []string{*value}}
143 body, err := json.Marshal(rrs)
144 if err != nil {
145 return fmt.Errorf("cannot marshall to json: %v", err)
146 }
147
148 url := fmt.Sprintf("%s/%s/TXT", c.gandiRecordsUrl(*domain), *name)
149 req, err := http.NewRequest("PUT", url, bytes.NewReader(body))
150 if err != nil {
151 return err
152 }
153
154 req.Header.Set("Content-Type", "application/json")
155
156 status, _, err := c.doRequest(req, false)
157 if err != nil {
158 return err
159 }
160
161 if status != http.StatusCreated && status != http.StatusOK {
162 return fmt.Errorf("failed updating TXT record: %v", err)
163 }
164
165 return nil
166}
167
168func (c *GandiClient) DeleteTxtRecord(domain *string, name *string) error {
169 // curl -X DELETE -H "Content-Type: application/json" \
170 // -H "X-Api-Key: $APIKEY" \
171 // https://dns.api.gandi.net/api/v5/domains/<DOMAIN>/records/<NAME>/<TYPE>
172 url := fmt.Sprintf("%s/%s/TXT", c.gandiRecordsUrl(*domain), *name)
173 req, err := http.NewRequest("DELETE", url, nil)
174 if err != nil {
175 return err
176 }
177
178 status, _, err := c.doRequest(req, false)
179 if err != nil {
180 return err
181 }
182
183 if status != http.StatusOK && status != http.StatusNoContent {
184 return fmt.Errorf("failed deleting TXT record: %v", err)
185 }
186
187 return nil
188}
189