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
64 changes: 35 additions & 29 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"github.com/libdns/libdns"
"io"
"log"
"net/http"
"net/url"
"runtime"
"strconv"
"strings"

"github.com/libdns/libdns"
)

func (p *Provider) getZoneRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
Expand Down Expand Up @@ -80,7 +81,8 @@ func (p *Provider) getZoneRecords(ctx context.Context, zone string) ([]libdns.Re
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)
rr := libDnsRecord.RR()
fmt.Printf("[%s] unsupported record conversion of type %v: %v\n", p.caller(callerSkipDepth), rr.Type, rr.Name)
continue
default:
return nil, err
Expand All @@ -99,7 +101,7 @@ 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)
return libdns.Record{}, err
return nil, err
}

reqURL.Path = "/CMD_API_DNS_CONTROL"
Expand All @@ -110,24 +112,25 @@ func (p *Provider) appendZoneRecord(ctx context.Context, zone string, record lib
queryString.Set("full_mx_records", "yes")
queryString.Set("allow_dns_underscore", "yes")
queryString.Set("domain", zone)
queryString.Set("type", record.Type)
queryString.Set("name", record.Name)
queryString.Set("value", record.Value)

if record.Type != "NS" {
queryString.Set("ttl", strconv.Itoa(int(record.TTL.Seconds())))
rr := record.RR()
queryString.Set("type", rr.Type)
queryString.Set("name", rr.Name)
queryString.Set("value", rr.Data)

if rr.Type != "NS" {
queryString.Set("ttl", strconv.Itoa(int(rr.TTL.Seconds())))
}

reqURL.RawQuery = queryString.Encode()

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

record.ID = fmt.Sprintf("name=%v&value=%v", record.Name, record.Value)

return record, nil
rr.Data = fmt.Sprintf("name=%v&value=%v", rr.Name, rr.Data)
return &rr, nil
}

func (p *Provider) setZoneRecord(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) {
Expand All @@ -137,7 +140,7 @@ 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)
return libdns.Record{}, err
return nil, err
}

reqURL.Path = "/CMD_API_DNS_CONTROL"
Expand All @@ -146,18 +149,21 @@ func (p *Provider) setZoneRecord(ctx context.Context, zone string, record libdns
queryString.Set("action", "edit")
queryString.Set("json", "yes")
queryString.Set("domain", zone)
queryString.Set("type", record.Type)
queryString.Set("name", record.Name)
queryString.Set("value", record.Value)

if record.Type != "NS" {
queryString.Set("ttl", strconv.Itoa(int(record.TTL.Seconds())))
rr := record.RR()
queryString.Set("type", rr.Type)
queryString.Set("name", rr.Name)
queryString.Set("value", rr.Data)

if rr.Type != "NS" {
queryString.Set("ttl", strconv.Itoa(int(rr.TTL.Seconds())))
}

existingRecords, _ := p.getZoneRecords(ctx, zone)
var existingRecordIndex = -1
for i := range existingRecords {
if existingRecords[i].Name == record.Name && existingRecords[i].Type == record.Type {
existingRR := existingRecords[i].RR()
if existingRR.Name == rr.Name && existingRR.Type == rr.Type {
existingRecordIndex = i
break
}
Expand All @@ -166,21 +172,20 @@ func (p *Provider) setZoneRecord(ctx context.Context, zone string, record libdns
// If we're not -1, we found a matching existing record. This changes the API call
// from create only to edit.
if existingRecordIndex != -1 {
editKey := fmt.Sprintf("%vrecs0", strings.ToLower(record.Type))
editValue := existingRecords[existingRecordIndex].ID
editKey := fmt.Sprintf("%vrecs0", strings.ToLower(rr.Type))
editValue := existingRecords[existingRecordIndex].RR().Data
queryString.Set(editKey, editValue)
}

reqURL.RawQuery = queryString.Encode()

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

record.ID = fmt.Sprintf("name=%v&value=%v", record.Name, record.Value)

return record, nil
rr.Data = fmt.Sprintf("name=%v&value=%v", rr.Name, rr.Data)
return &rr, nil
}

func (p *Provider) deleteZoneRecord(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) {
Expand All @@ -190,7 +195,7 @@ 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)
return libdns.Record{}, err
return nil, err
}

reqURL.Path = "/CMD_API_DNS_CONTROL"
Expand All @@ -200,15 +205,16 @@ func (p *Provider) deleteZoneRecord(ctx context.Context, zone string, record lib
queryString.Set("json", "yes")
queryString.Set("domain", zone)

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

reqURL.RawQuery = queryString.Encode()

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

return record, nil
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ module github.com/libdns/directadmin

go 1.18

require github.com/libdns/libdns v0.2.2
require github.com/libdns/libdns v1.1.0

require github.com/joho/godotenv v1.5.1
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU=
github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
83 changes: 59 additions & 24 deletions models.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package directadmin
import (
"errors"
"fmt"
"github.com/libdns/libdns"
"strconv"
"strings"
"time"

"github.com/libdns/libdns"
)

type daZone struct {
Expand Down Expand Up @@ -40,40 +41,74 @@ type daRecord struct {
var ErrUnsupported = errors.New("unsupported record type")

func (r daRecord) libdnsRecord(zone string) (libdns.Record, error) {
record := libdns.Record{
ID: r.Combined,
Type: r.Type,
var ttl time.Duration
if len(r.TTL) > 0 {
ttlVal, err := strconv.Atoi(r.TTL)
if err != nil {
return nil, fmt.Errorf("failed to parse TTL for %v: %v", r.Name, err)
}
ttl = time.Duration(ttlVal) * time.Second
}

rr := libdns.RR{
Name: r.Name,
Type: r.Type,
TTL: ttl,
Data: r.Value,
}

switch r.Type {
case "MX":
splits := strings.Split(r.Value, " ")

priority, err := strconv.Atoi(splits[0])
if err != nil {
return record, fmt.Errorf("failed to parse MX priority for %v: %v", r.Name, err)
if len(splits) >= 2 {
priority, err := strconv.Atoi(splits[0])
if err == nil {
return &libdns.MX{
Name: r.Name,
TTL: ttl,
Preference: uint16(priority),
Target: fmt.Sprintf("%v.%v", splits[1], zone),
}, nil
}
}

record.Priority = uint(priority)
record.Value = fmt.Sprintf("%v.%v", splits[1], zone)
return &rr, nil
case "SRV":
return record, ErrUnsupported
case "URI":
return record, ErrUnsupported
default:
record.Value = r.Value
}
// Parse SRV record format: "priority weight port target"
splits := strings.Split(r.Value, " ")
if len(splits) >= 4 {
priority, err1 := strconv.Atoi(splits[0])
weight, err2 := strconv.Atoi(splits[1])
port, err3 := strconv.Atoi(splits[2])
if err1 == nil && err2 == nil && err3 == nil {
// Extract service and transport from the name
// SRV names are typically in format: _service._transport.name
nameParts := strings.Split(r.Name, ".")
if len(nameParts) >= 3 && strings.HasPrefix(nameParts[0], "_") && strings.HasPrefix(nameParts[1], "_") {
service := strings.TrimPrefix(nameParts[0], "_")
transport := strings.TrimPrefix(nameParts[1], "_")
// Reconstruct the base name (without service and transport prefixes)
baseName := strings.Join(nameParts[2:], ".")

if len(r.TTL) > 0 {
ttl, err := strconv.Atoi(r.TTL)
if err != nil {
return record, fmt.Errorf("failed to parse TTL for %v: %v", r.Name, err)
return &libdns.SRV{
Service: service,
Transport: transport,
Name: baseName,
TTL: ttl,
Priority: uint16(priority),
Weight: uint16(weight),
Port: uint16(port),
Target: splits[3],
}, nil
}
}
}
record.TTL = time.Duration(ttl) * time.Second
// Fall back to generic RR if parsing fails
return &rr, nil
case "URI":
return nil, ErrUnsupported
default:
return &rr, nil
}

return record, nil
}

type daResponse struct {
Expand Down
15 changes: 14 additions & 1 deletion provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package directadmin

import (
"context"
"fmt"
"strings"
"sync"

Expand Down Expand Up @@ -78,14 +79,26 @@ func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns
zone = strings.TrimSuffix(zone, ".")

var updated []libdns.Record
var errors []error

for _, rec := range records {
result, err := p.setZoneRecord(ctx, zone, rec)
if err != nil {
return nil, err
errors = append(errors, err)
continue
}
updated = append(updated, result)
}

if len(errors) > 0 {
if len(updated) == 0 {
// No records were updated, return AtomicErr
return nil, libdns.AtomicErr(fmt.Errorf("all records failed to update: %v", errors))
}
// Some records were updated, return a combined error
return updated, fmt.Errorf("partial update failed: %v", errors)
}

return updated, nil
}

Expand Down
Loading