Skip to content

Commit 9692f98

Browse files
authored
Fix root zone bug and add tests (#4)
1 parent 539d7a8 commit 9692f98

File tree

4 files changed

+200
-63
lines changed

4 files changed

+200
-63
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
LIBDNS_DA_TEST_ZONE=domain.com.
2+
LIBDNS_DA_NON_ROOT_TEST_ZONE=sub.domain.com
23
LIBDNS_DA_TEST_SERVER_URL=https://da.domain.com:2222
34
LIBDNS_DA_TEST_INSECURE_SERVER_URL=https://1.1.1.1:2222
45
LIBDNS_DA_TEST_USER=admin

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,21 @@ The `CMD_API_SHOW_DOMAINS` permission is needed to get the zone ID, the `CMD_API
3636

3737
If you're only using the `GetRecords()` method, you can remove the `CMD_API_DNS_CONTROL` permission to guarantee no changes will be made.
3838

39-
![Screenshot of login key settings](./assets/login-key-options.png)
39+
![Screenshot of login key settings](./assets/login-key-options.png)
40+
41+
## Running Tests
42+
43+
Please note that these tests **must** run against a real direct admin (DA) DNS API.
44+
45+
You should **_never_** run these tests against an in use, production zone.
46+
47+
To run these tests, you need to copy .env.example to .env and modify the values for your environment.
48+
49+
| ENV Var | Description |
50+
|--------------|--------------|
51+
| `LIBDNS_DA_TEST_ZONE` | should be a root zone on the DA server |
52+
| `LIBDNS_DA_NON_ROOT_TEST_ZONE` | should be a non existing subdomain off of a root zoon on the DA server |
53+
| `LIBDNS_DA_TEST_SERVER_URL` | should be a url with a valid TLS certificate |
54+
| `LIBDNS_DA_TEST_INSECURE_SERVER_URL` | should likely be the direct IP url for your DA server |
55+
| `LIBDNS_DA_TEST_USER` | user with API access |
56+
| `LIBDNS_DA_TEST_LOGIN_KEY` | key for user |

client.go

Lines changed: 126 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,43 @@ import (
44
"context"
55
"crypto/tls"
66
"encoding/json"
7+
"errors"
78
"fmt"
89
"github.com/libdns/libdns"
910
"io"
1011
"log"
1112
"net/http"
1213
"net/url"
13-
"runtime"
1414
"strconv"
1515
"strings"
1616
)
1717

1818
func (p *Provider) getZoneRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
19-
callerSkipDepth := 2
20-
2119
reqURL, err := url.Parse(p.ServerURL)
2220
if err != nil {
23-
fmt.Printf("[%s] failed to parse server url: %v\n", p.caller(callerSkipDepth), err)
21+
fmt.Printf("failed to parse server url: %v\n", err)
2422
return nil, err
2523
}
2624

25+
rootZone, err := p.findRoot(ctx, zone)
26+
if err != nil {
27+
rootZone = zone
28+
}
29+
2730
reqURL.Path = "/CMD_API_DNS_CONTROL"
2831

2932
queryString := make(url.Values)
3033
queryString.Set("json", "yes")
3134
queryString.Set("full_mx_records", "yes")
3235
queryString.Set("allow_dns_underscore", "yes")
3336
queryString.Set("ttl", "yes")
34-
queryString.Set("domain", zone)
37+
queryString.Set("domain", rootZone)
3538

3639
reqURL.RawQuery = queryString.Encode()
3740

3841
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
3942
if err != nil {
40-
fmt.Printf("[%s] failed to build new request: %v\n", p.caller(callerSkipDepth), err)
43+
fmt.Printf("failed to build new request: %v\n", err)
4144
return nil, err
4245
}
4346

@@ -52,35 +55,35 @@ func (p *Provider) getZoneRecords(ctx context.Context, zone string) ([]libdns.Re
5255

5356
resp, err := client.Do(req)
5457
if err != nil {
55-
fmt.Printf("[%s] failed to execute request: %v\n", p.caller(callerSkipDepth), err)
58+
fmt.Printf("failed to execute request: %v\n", err)
5659
return nil, err
5760
}
5861
defer func(Body io.ReadCloser) {
5962
err := Body.Close()
6063
if err != nil {
61-
fmt.Printf("[%s] failed to close body: %v\n", p.caller(callerSkipDepth), err)
64+
fmt.Printf("failed to close body: %v\n", err)
6265
}
6366
}(resp.Body)
6467

6568
if resp.StatusCode != http.StatusOK {
66-
fmt.Printf("[%s] api response error, status code: %v\n", p.caller(callerSkipDepth), resp.StatusCode)
69+
fmt.Printf("api response error, status code: %v\n", resp.StatusCode)
6770
return nil, err
6871
}
6972

7073
var respData daZone
7174
err = json.NewDecoder(resp.Body).Decode(&respData)
7275
if err != nil {
73-
fmt.Printf("[%s] failed to json decode response: %v\n", p.caller(callerSkipDepth), err)
76+
fmt.Printf("failed to json decode response: %v\n", err)
7477
return nil, err
7578
}
7679

7780
recs := make([]libdns.Record, 0, len(respData.Records))
7881
for i := range respData.Records {
7982
libDnsRecord, err := respData.Records[i].libdnsRecord(zone)
8083
if err != nil {
81-
switch err {
82-
case ErrUnsupported:
83-
fmt.Printf("[%s] unsupported record conversion of type %v: %v\n", p.caller(callerSkipDepth), libDnsRecord.Type, libDnsRecord.Name)
84+
switch {
85+
case errors.Is(err, ErrUnsupported):
86+
fmt.Printf("unsupported record conversion of type %v: %v\n", libDnsRecord.Type, libDnsRecord.Name)
8487
continue
8588
default:
8689
return nil, err
@@ -98,18 +101,23 @@ func (p *Provider) appendZoneRecord(ctx context.Context, zone string, record lib
98101

99102
reqURL, err := url.Parse(p.ServerURL)
100103
if err != nil {
101-
fmt.Printf("[%s] failed to parse server url: %v\n", p.caller(2), err)
104+
fmt.Printf("failed to parse server url: %v\n", err)
102105
return libdns.Record{}, err
103106
}
104107

108+
rootZone, err := p.findRoot(ctx, zone)
109+
if err != nil {
110+
rootZone = zone
111+
}
112+
105113
reqURL.Path = "/CMD_API_DNS_CONTROL"
106114

107115
queryString := make(url.Values)
108116
queryString.Set("action", "add")
109117
queryString.Set("json", "yes")
110118
queryString.Set("full_mx_records", "yes")
111119
queryString.Set("allow_dns_underscore", "yes")
112-
queryString.Set("domain", zone)
120+
queryString.Set("domain", rootZone)
113121
queryString.Set("type", record.Type)
114122
queryString.Set("name", record.Name)
115123
queryString.Set("value", record.Value)
@@ -120,7 +128,7 @@ func (p *Provider) appendZoneRecord(ctx context.Context, zone string, record lib
120128

121129
reqURL.RawQuery = queryString.Encode()
122130

123-
err = p.executeRequest(ctx, http.MethodGet, reqURL.String())
131+
err = p.executeJsonRequest(ctx, http.MethodGet, reqURL.String())
124132
if err != nil {
125133
return libdns.Record{}, err
126134
}
@@ -136,16 +144,21 @@ func (p *Provider) setZoneRecord(ctx context.Context, zone string, record libdns
136144

137145
reqURL, err := url.Parse(p.ServerURL)
138146
if err != nil {
139-
fmt.Printf("[%s] failed to parse server url: %v\n", p.caller(2), err)
147+
fmt.Printf("failed to parse server url: %v\n", err)
140148
return libdns.Record{}, err
141149
}
142150

151+
rootZone, err := p.findRoot(ctx, zone)
152+
if err != nil {
153+
rootZone = zone
154+
}
155+
143156
reqURL.Path = "/CMD_API_DNS_CONTROL"
144157

145158
queryString := make(url.Values)
146159
queryString.Set("action", "edit")
147160
queryString.Set("json", "yes")
148-
queryString.Set("domain", zone)
161+
queryString.Set("domain", rootZone)
149162
queryString.Set("type", record.Type)
150163
queryString.Set("name", record.Name)
151164
queryString.Set("value", record.Value)
@@ -173,7 +186,7 @@ func (p *Provider) setZoneRecord(ctx context.Context, zone string, record libdns
173186

174187
reqURL.RawQuery = queryString.Encode()
175188

176-
err = p.executeRequest(ctx, http.MethodGet, reqURL.String())
189+
err = p.executeJsonRequest(ctx, http.MethodGet, reqURL.String())
177190
if err != nil {
178191
return libdns.Record{}, err
179192
}
@@ -189,40 +202,116 @@ func (p *Provider) deleteZoneRecord(ctx context.Context, zone string, record lib
189202

190203
reqURL, err := url.Parse(p.ServerURL)
191204
if err != nil {
192-
fmt.Printf("[%s] failed to parse server url: %v\n", p.caller(2), err)
205+
fmt.Printf("failed to parse server url: %v\n", err)
193206
return libdns.Record{}, err
194207
}
195208

209+
rootZone, err := p.findRoot(ctx, zone)
210+
if err != nil {
211+
rootZone = zone
212+
}
213+
196214
reqURL.Path = "/CMD_API_DNS_CONTROL"
197215

198216
queryString := make(url.Values)
199217
queryString.Set("action", "select")
200218
queryString.Set("json", "yes")
201-
queryString.Set("domain", zone)
219+
queryString.Set("domain", rootZone)
202220

203221
editKey := fmt.Sprintf("%vrecs0", strings.ToLower(record.Type))
204222
editValue := fmt.Sprintf("name=%v&value=%v", record.Name, record.Value)
205223
queryString.Set(editKey, editValue)
206224

207225
reqURL.RawQuery = queryString.Encode()
208226

209-
err = p.executeRequest(ctx, http.MethodGet, reqURL.String())
227+
err = p.executeJsonRequest(ctx, http.MethodGet, reqURL.String())
210228
if err != nil {
211229
return libdns.Record{}, err
212230
}
213231

214232
return record, nil
215233
}
216234

217-
func (p *Provider) executeRequest(ctx context.Context, method, url string) error {
218-
callerSkipDepth := 3
235+
func (p *Provider) findRoot(ctx context.Context, zone string) (string, error) {
236+
reqURL, err := url.Parse(p.ServerURL)
237+
if err != nil {
238+
fmt.Printf("failed to parse server url: %v\n", err)
239+
return "", err
240+
}
241+
242+
reqURL.Path = "/CMD_API_SHOW_DOMAINS"
219243

220-
req, err := http.NewRequestWithContext(ctx, method, url, nil)
244+
resp, err := p.executeQueryRequest(ctx, http.MethodGet, reqURL.String())
245+
if err != nil {
246+
return "", err
247+
}
248+
249+
zoneParts := strings.Split(zone, ".")
250+
251+
// Limit to 100 rounds
252+
for i := 0; i < 100; i++ {
253+
for _, value := range resp {
254+
if value == strings.Join(zoneParts, ".") {
255+
return value, nil
256+
}
257+
}
258+
259+
zoneParts = zoneParts[1:]
260+
}
261+
262+
return "", errors.New("root zone not found")
263+
}
264+
265+
func (p *Provider) executeJsonRequest(ctx context.Context, method, requestUrl string) error {
266+
resp, err := p.doRequest(ctx, method, requestUrl)
221267
if err != nil {
222-
fmt.Printf("[%s] failed to build new request: %v\n", p.caller(callerSkipDepth), err)
223268
return err
224269
}
225270

271+
var respData daResponse
272+
err = json.Unmarshal(resp, &respData)
273+
if err != nil {
274+
fmt.Printf("failed to json decode response: %v\n", err)
275+
return err
276+
}
277+
278+
if len(respData.Error) > 0 {
279+
trimmedResult := strings.Split(respData.Result, "\n")[0]
280+
fmt.Printf("api response error: %v: %v\n", respData.Error, trimmedResult)
281+
return fmt.Errorf("api response error: %v: %v\n", respData.Error, trimmedResult)
282+
}
283+
284+
return nil
285+
}
286+
287+
func (p *Provider) executeQueryRequest(ctx context.Context, method, requestUrl string) ([]string, error) {
288+
resp, err := p.doRequest(ctx, method, requestUrl)
289+
if err != nil {
290+
return nil, err
291+
}
292+
293+
params, err := url.ParseQuery(string(resp))
294+
if err != nil {
295+
return nil, err
296+
}
297+
298+
var domains []string
299+
for _, param := range params {
300+
for _, domain := range param {
301+
domains = append(domains, domain)
302+
}
303+
}
304+
305+
return domains, nil
306+
}
307+
308+
func (p *Provider) doRequest(ctx context.Context, method, url string) ([]byte, error) {
309+
req, err := http.NewRequestWithContext(ctx, method, url, nil)
310+
if err != nil {
311+
fmt.Printf("failed to build new request: %v\n", err)
312+
return nil, err
313+
}
314+
226315
req.SetBasicAuth(p.User, p.LoginKey)
227316

228317
client := &http.Client{
@@ -234,48 +323,32 @@ func (p *Provider) executeRequest(ctx context.Context, method, url string) error
234323

235324
resp, err := client.Do(req)
236325
if err != nil {
237-
fmt.Printf("[%s] failed to execute request: %v\n", p.caller(callerSkipDepth), err)
238-
return err
326+
fmt.Printf("failed to execute request: %v\n", err)
327+
return nil, err
239328
}
240329
defer func(Body io.ReadCloser) {
241330
err := Body.Close()
242331
if err != nil {
243-
fmt.Printf("[%s] failed to close body: %v\n", p.caller(callerSkipDepth), err)
332+
fmt.Printf("failed to close body: %v\n", err)
244333
}
245334
}(resp.Body)
246335

247-
var respData daResponse
248-
err = json.NewDecoder(resp.Body).Decode(&respData)
249-
if err != nil {
250-
fmt.Printf("[%s] failed to json decode response: %v\n", p.caller(callerSkipDepth), err)
251-
return err
252-
}
253-
254-
if len(respData.Error) > 0 {
255-
trimmedResult := strings.Split(respData.Result, "\n")[0]
256-
fmt.Printf("[%s] api response error: %v: %v\n", p.caller(callerSkipDepth), respData.Error, trimmedResult)
257-
return fmt.Errorf("[%s] api response error: %v: %v\n", p.caller(callerSkipDepth), respData.Error, trimmedResult)
258-
}
259-
260336
if resp.StatusCode != http.StatusOK {
261337
bodyBytes, err := io.ReadAll(resp.Body)
262338
if err != nil {
263-
fmt.Printf("[%s] failed to read response body: %v\n", p.caller(callerSkipDepth), err)
264-
return err
339+
fmt.Printf("failed to read response body: %v\n", err)
340+
return nil, err
265341
}
266342
bodyString := string(bodyBytes)
267343
log.Println(bodyString)
268344

269-
return err
345+
return nil, err
270346
}
271347

272-
return nil
273-
}
348+
bodyBytes, err := io.ReadAll(resp.Body)
349+
if err != nil {
350+
return nil, err
351+
}
274352

275-
func (p *Provider) caller(skip int) string {
276-
pc := make([]uintptr, 15)
277-
n := runtime.Callers(skip, pc)
278-
frames := runtime.CallersFrames(pc[:n])
279-
frame, _ := frames.Next()
280-
return frame.Function
353+
return bodyBytes, nil
281354
}

0 commit comments

Comments
 (0)