Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
LIBDNS_DA_TEST_ZONE=domain.com.
LIBDNS_DA_NON_ROOT_TEST_ZONE=sub.domain.com
LIBDNS_DA_TEST_SERVER_URL=https://da.domain.com:2222
LIBDNS_DA_TEST_INSECURE_SERVER_URL=https://1.1.1.1:2222
LIBDNS_DA_TEST_USER=admin
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,21 @@ The `CMD_API_SHOW_DOMAINS` permission is needed to get the zone ID, the `CMD_API

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

![Screenshot of login key settings](./assets/login-key-options.png)
![Screenshot of login key settings](./assets/login-key-options.png)

## Running Tests

Please note that these tests **must** run against a real direct admin (DA) DNS API.

You should **_never_** run these tests against an in use, production zone.

To run these tests, you need to copy .env.example to .env and modify the values for your environment.

| ENV Var | Description |
|--------------|--------------|
| `LIBDNS_DA_TEST_ZONE` | should be a root zone on the DA server |
| `LIBDNS_DA_NON_ROOT_TEST_ZONE` | should be a non existing subdomain off of a root zoon on the DA server |
| `LIBDNS_DA_TEST_SERVER_URL` | should be a url with a valid TLS certificate |
| `LIBDNS_DA_TEST_INSECURE_SERVER_URL` | should likely be the direct IP url for your DA server |
| `LIBDNS_DA_TEST_USER` | user with API access |
| `LIBDNS_DA_TEST_LOGIN_KEY` | key for user |
179 changes: 126 additions & 53 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,43 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"github.com/libdns/libdns"
"io"
"log"
"net/http"
"net/url"
"runtime"
"strconv"
"strings"
)

func (p *Provider) getZoneRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
callerSkipDepth := 2

reqURL, err := url.Parse(p.ServerURL)
if err != nil {
fmt.Printf("[%s] failed to parse server url: %v\n", p.caller(callerSkipDepth), err)
fmt.Printf("failed to parse server url: %v\n", err)
return nil, err
}

rootZone, err := p.findRoot(ctx, zone)
if err != nil {
rootZone = zone
}

reqURL.Path = "/CMD_API_DNS_CONTROL"

queryString := make(url.Values)
queryString.Set("json", "yes")
queryString.Set("full_mx_records", "yes")
queryString.Set("allow_dns_underscore", "yes")
queryString.Set("ttl", "yes")
queryString.Set("domain", zone)
queryString.Set("domain", rootZone)

reqURL.RawQuery = queryString.Encode()

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

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

resp, err := client.Do(req)
if err != nil {
fmt.Printf("[%s] failed to execute request: %v\n", p.caller(callerSkipDepth), err)
fmt.Printf("failed to execute request: %v\n", err)
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
fmt.Printf("[%s] failed to close body: %v\n", p.caller(callerSkipDepth), err)
fmt.Printf("failed to close body: %v\n", err)
}
}(resp.Body)

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

var respData daZone
err = json.NewDecoder(resp.Body).Decode(&respData)
if err != nil {
fmt.Printf("[%s] failed to json decode response: %v\n", p.caller(callerSkipDepth), err)
fmt.Printf("failed to json decode response: %v\n", err)
return nil, err
}

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

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

rootZone, err := p.findRoot(ctx, zone)
if err != nil {
rootZone = zone
}

reqURL.Path = "/CMD_API_DNS_CONTROL"

queryString := make(url.Values)
queryString.Set("action", "add")
queryString.Set("json", "yes")
queryString.Set("full_mx_records", "yes")
queryString.Set("allow_dns_underscore", "yes")
queryString.Set("domain", zone)
queryString.Set("domain", rootZone)
queryString.Set("type", record.Type)
queryString.Set("name", record.Name)
queryString.Set("value", record.Value)
Expand All @@ -120,7 +128,7 @@ func (p *Provider) appendZoneRecord(ctx context.Context, zone string, record lib

reqURL.RawQuery = queryString.Encode()

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

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

rootZone, err := p.findRoot(ctx, zone)
if err != nil {
rootZone = zone
}

reqURL.Path = "/CMD_API_DNS_CONTROL"

queryString := make(url.Values)
queryString.Set("action", "edit")
queryString.Set("json", "yes")
queryString.Set("domain", zone)
queryString.Set("domain", rootZone)
queryString.Set("type", record.Type)
queryString.Set("name", record.Name)
queryString.Set("value", record.Value)
Expand Down Expand Up @@ -173,7 +186,7 @@ func (p *Provider) setZoneRecord(ctx context.Context, zone string, record libdns

reqURL.RawQuery = queryString.Encode()

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

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

rootZone, err := p.findRoot(ctx, zone)
if err != nil {
rootZone = zone
}

reqURL.Path = "/CMD_API_DNS_CONTROL"

queryString := make(url.Values)
queryString.Set("action", "select")
queryString.Set("json", "yes")
queryString.Set("domain", zone)
queryString.Set("domain", rootZone)

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

reqURL.RawQuery = queryString.Encode()

err = p.executeRequest(ctx, http.MethodGet, reqURL.String())
err = p.executeJsonRequest(ctx, http.MethodGet, reqURL.String())
if err != nil {
return libdns.Record{}, err
}

return record, nil
}

func (p *Provider) executeRequest(ctx context.Context, method, url string) error {
callerSkipDepth := 3
func (p *Provider) findRoot(ctx context.Context, zone string) (string, error) {
reqURL, err := url.Parse(p.ServerURL)
if err != nil {
fmt.Printf("failed to parse server url: %v\n", err)
return "", err
}

reqURL.Path = "/CMD_API_SHOW_DOMAINS"

req, err := http.NewRequestWithContext(ctx, method, url, nil)
resp, err := p.executeQueryRequest(ctx, http.MethodGet, reqURL.String())
if err != nil {
return "", err
}

zoneParts := strings.Split(zone, ".")

// Limit to 100 rounds
for i := 0; i < 100; i++ {
for _, value := range resp {
if value == strings.Join(zoneParts, ".") {
return value, nil
}
}

zoneParts = zoneParts[1:]
}

return "", errors.New("root zone not found")
}

func (p *Provider) executeJsonRequest(ctx context.Context, method, requestUrl string) error {
resp, err := p.doRequest(ctx, method, requestUrl)
if err != nil {
fmt.Printf("[%s] failed to build new request: %v\n", p.caller(callerSkipDepth), err)
return err
}

var respData daResponse
err = json.Unmarshal(resp, &respData)
if err != nil {
fmt.Printf("failed to json decode response: %v\n", err)
return err
}

if len(respData.Error) > 0 {
trimmedResult := strings.Split(respData.Result, "\n")[0]
fmt.Printf("api response error: %v: %v\n", respData.Error, trimmedResult)
return fmt.Errorf("api response error: %v: %v\n", respData.Error, trimmedResult)
}

return nil
}

func (p *Provider) executeQueryRequest(ctx context.Context, method, requestUrl string) ([]string, error) {
resp, err := p.doRequest(ctx, method, requestUrl)
if err != nil {
return nil, err
}

params, err := url.ParseQuery(string(resp))
if err != nil {
return nil, err
}

var domains []string
for _, param := range params {
for _, domain := range param {
domains = append(domains, domain)
}
}

return domains, nil
}

func (p *Provider) doRequest(ctx context.Context, method, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, method, url, nil)
if err != nil {
fmt.Printf("failed to build new request: %v\n", err)
return nil, err
}

req.SetBasicAuth(p.User, p.LoginKey)

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

resp, err := client.Do(req)
if err != nil {
fmt.Printf("[%s] failed to execute request: %v\n", p.caller(callerSkipDepth), err)
return err
fmt.Printf("failed to execute request: %v\n", err)
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
fmt.Printf("[%s] failed to close body: %v\n", p.caller(callerSkipDepth), err)
fmt.Printf("failed to close body: %v\n", err)
}
}(resp.Body)

var respData daResponse
err = json.NewDecoder(resp.Body).Decode(&respData)
if err != nil {
fmt.Printf("[%s] failed to json decode response: %v\n", p.caller(callerSkipDepth), err)
return err
}

if len(respData.Error) > 0 {
trimmedResult := strings.Split(respData.Result, "\n")[0]
fmt.Printf("[%s] api response error: %v: %v\n", p.caller(callerSkipDepth), respData.Error, trimmedResult)
return fmt.Errorf("[%s] api response error: %v: %v\n", p.caller(callerSkipDepth), respData.Error, trimmedResult)
}

if resp.StatusCode != http.StatusOK {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("[%s] failed to read response body: %v\n", p.caller(callerSkipDepth), err)
return err
fmt.Printf("failed to read response body: %v\n", err)
return nil, err
}
bodyString := string(bodyBytes)
log.Println(bodyString)

return err
return nil, err
}

return nil
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

func (p *Provider) caller(skip int) string {
pc := make([]uintptr, 15)
n := runtime.Callers(skip, pc)
frames := runtime.CallersFrames(pc[:n])
frame, _ := frames.Next()
return frame.Function
return bodyBytes, nil
}
Loading