Compare commits

..

5 Commits
v0.1.1 ... main

11 changed files with 224 additions and 55 deletions

View File

@ -39,7 +39,7 @@ Usage of cloudflare-dns-cli:
IP address (default 127.0.0.1)
-m Set mx record with cname value
-o string
Path to save all records as json, e.g. ./records.json
Path to save all records as json, e.g. ./records.json, '-' for stdout
-s string
Subdomain, e.g. blog. Use @ for root (default "<UNSET>")
-x Delete records of subdomain

6
go.mod
View File

@ -1,8 +1,8 @@
module go.balki.me/cloudflare-dns-cli
go 1.21
go 1.22.2
require (
github.com/libdns/cloudflare v0.1.0
github.com/libdns/libdns v0.2.1
github.com/libdns/cloudflare v0.1.1
github.com/libdns/libdns v0.2.2
)

9
go.sum
View File

@ -1,5 +1,4 @@
github.com/libdns/cloudflare v0.1.0 h1:93WkJaGaiXCe353LHEP36kAWCUw0YjFqwhkBkU2/iic=
github.com/libdns/cloudflare v0.1.0/go.mod h1:a44IP6J1YH6nvcNl1PverfJviADgXUnsozR3a7vBKN8=
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=

35
main.go
View File

@ -33,9 +33,10 @@ func genRecord() (libdns.Record, error) {
case cname != "":
if mx {
return libdns.Record{
Type: "MX",
Name: sub,
Value: cname,
Type: "MX",
Name: sub,
Value: cname,
Priority: 10,
}, nil
} else {
return libdns.Record{
@ -63,9 +64,9 @@ func genRecord() (libdns.Record, error) {
func selectRecordsToDelete(recs []libdns.Record) []libdns.Record {
name := func() string {
if sub == "@" {
return domain
return ""
}
return fmt.Sprintf("%s.%s", sub, domain)
return sub
}()
recs = func() (result []libdns.Record) {
for _, r := range recs {
@ -75,12 +76,16 @@ func selectRecordsToDelete(recs []libdns.Record) []libdns.Record {
}
return
}()
fmt.Printf("Records matching %s\n", name)
if len(recs) == 0 {
fmt.Printf("No Records match %s\n", name)
return nil
}
fmt.Printf("Records matching %s\n\n", name)
for i, r := range recs {
fmt.Printf("%5d %6s %s\n", i, r.Type, r.Value)
}
var delRange string
fmt.Print("Enter record indexes seperated by comma(,) to delete. Use hyphen(-) for a closed range. Also can be all or none\nIndexes: ")
fmt.Print("\nEnter record indexes seperated by comma(,) to delete. Use hyphen(-) for a closed range. Also can be all or none\nIndexes: ")
if _, err := fmt.Scanln(&delRange); err != nil {
log.Panicln(err)
}
@ -108,7 +113,7 @@ func main() {
flag.StringVar(&token, "a", token, "Cloudflare API Token. env var: CF_TOKEN")
flag.TextVar(&ip, "i", ip, "IP address")
flag.StringVar(&cname, "c", cname, "CNAME target")
flag.StringVar(&path, "o", path, "Path to save all records as json, e.g. ./records.json")
flag.StringVar(&path, "o", path, "Path to save all records as json, e.g. ./records.json, '-' for stdout")
flag.BoolVar(&del, "x", del, "Delete records of subdomain")
flag.BoolVar(&mx, "m", mx, "Set mx record with cname value")
flag.Parse()
@ -165,14 +170,20 @@ func main() {
if err != nil {
log.Panicln(err)
}
data, err := json.Marshal(recs)
data, err := json.Marshal(recs)
if err != nil {
log.Panicln(err)
}
err = os.WriteFile(path, data, 0644)
if err != nil {
log.Panicln(err)
if path == "-" {
if _, err = os.Stdout.Write(data); err != nil {
log.Panicln(err)
}
} else {
if err = os.WriteFile(path, data, 0644); err != nil {
log.Panicln(err)
}
}
}
}

View File

@ -14,6 +14,14 @@ func TestParseRange(t *testing.T) {
}
}
func TestParseRangeEmpty(t *testing.T) {
expectedLen := 0
actual := parseRange("", 10)
if expectedLen != len(actual) {
t.Errorf("unexpected %#v\n", actual)
}
}
func TestParseIP(t *testing.T) {
var ip net.IP
if err := ip.UnmarshalText([]byte("127.0.0.1")); err != nil {

View File

@ -12,7 +12,11 @@ import (
)
func (p *Provider) createRecord(ctx context.Context, zoneInfo cfZone, record libdns.Record) (cfDNSRecord, error) {
jsonBytes, err := json.Marshal(cloudflareRecord(record))
cfRec, err := cloudflareRecord(record)
if err != nil {
return cfDNSRecord{}, err
}
jsonBytes, err := json.Marshal(cfRec)
if err != nil {
return cfDNSRecord{}, err
}

View File

@ -2,6 +2,7 @@ package cloudflare
import (
"encoding/json"
"strings"
"time"
"github.com/libdns/libdns"
@ -61,7 +62,8 @@ type cfDNSRecord struct {
ZoneName string `json:"zone_name,omitempty"`
CreatedOn time.Time `json:"created_on,omitempty"`
ModifiedOn time.Time `json:"modified_on,omitempty"`
Data *struct {
Priority uint `json:"priority,omitempty"`
Data struct {
// LOC
LatDegrees int `json:"lat_degrees,omitempty"`
LatMinutes int `json:"lat_minutes,omitempty"`
@ -80,9 +82,9 @@ type cfDNSRecord struct {
Service string `json:"service,omitempty"`
Proto string `json:"proto,omitempty"`
Name string `json:"name,omitempty"`
Priority int `json:"priority,omitempty"`
Weight int `json:"weight,omitempty"`
Port int `json:"port,omitempty"`
Priority uint `json:"priority,omitempty"`
Weight uint `json:"weight,omitempty"`
Port uint `json:"port,omitempty"`
Target string `json:"target,omitempty"`
// DNSKEY
@ -109,23 +111,57 @@ type cfDNSRecord struct {
}
func (r cfDNSRecord) libdnsRecord(zone string) libdns.Record {
return libdns.Record{
if r.Type == "SRV" {
srv := libdns.SRV{
Service: strings.TrimPrefix(r.Data.Service, "_"),
Proto: strings.TrimPrefix(r.Data.Proto, "_"),
Name: r.Data.Name,
Priority: r.Data.Priority,
Weight: r.Data.Weight,
Port: r.Data.Port,
Target: r.Data.Target,
}
return srv.ToRecord()
}
out := libdns.Record{
Type: r.Type,
Name: libdns.RelativeName(r.Name, zone),
Value: r.Content,
TTL: time.Duration(r.TTL) * time.Second,
ID: r.ID,
}
if r.Type == "MX" {
out.Priority = r.Priority
}
return out
}
func cloudflareRecord(r libdns.Record) cfDNSRecord {
return cfDNSRecord{
ID: r.ID,
Type: r.Type,
Name: r.Name,
Content: r.Value,
TTL: int(r.TTL.Seconds()),
func cloudflareRecord(r libdns.Record) (cfDNSRecord, error) {
rec := cfDNSRecord{
ID: r.ID,
Type: r.Type,
TTL: int(r.TTL.Seconds()),
}
if r.Type == "SRV" {
srv, err := r.ToSRV()
if err != nil {
return cfDNSRecord{}, err
}
rec.Data.Service = "_" + srv.Service
rec.Data.Priority = srv.Priority
rec.Data.Weight = srv.Weight
rec.Data.Proto = "_" + srv.Proto
rec.Data.Name = srv.Name
rec.Data.Port = srv.Port
rec.Data.Target = srv.Target
} else {
rec.Name = r.Name
rec.Content = r.Value
if r.Type == "MX" {
rec.Priority = r.Priority
}
}
return rec, nil
}
// All API responses have this structure.
@ -133,10 +169,14 @@ type cfResponse struct {
Result json.RawMessage `json:"result,omitempty"`
Success bool `json:"success"`
Errors []struct {
Code int `json:"code"`
Message string `json:"message"`
Code int `json:"code"`
Message string `json:"message"`
ErrorChain []struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error_chain,omitempty"`
} `json:"errors,omitempty"`
Messages []interface{} `json:"messages,omitempty"`
Messages []any `json:"messages,omitempty"`
ResultInfo *cfResultInfo `json:"result_info,omitempty"`
}

View File

@ -131,7 +131,10 @@ func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns
var results []libdns.Record
for _, rec := range records {
oldRec := cloudflareRecord(rec)
oldRec, err := cloudflareRecord(rec)
if err != nil {
return nil, err
}
oldRec.ZoneID = zoneInfo.ID
if rec.ID == "" {
// the record might already exist, even if we don't know the ID yet
@ -155,7 +158,11 @@ func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns
oldRec.ID = matches[0].ID
}
// record exists; update it
result, err := p.updateRecord(ctx, oldRec, cloudflareRecord(rec))
cfRec, err := cloudflareRecord(rec)
if err != nil {
return nil, err
}
result, err := p.updateRecord(ctx, oldRec, cfRec)
if err != nil {
return nil, err
}

View File

@ -41,14 +41,18 @@ recs, err := provider.GetRecords(ctx, zone)
// create records (AppendRecords is similar)
newRecs, err := provider.SetRecords(ctx, zone, []libdns.Record{
Type: "A",
Name: "sub",
Value: "1.2.3.4",
{
Type: "A",
Name: "sub",
Value: "1.2.3.4",
},
})
// delete records (this example uses provider-assigned ID)
deletedRecs, err := provider.DeleteRecords(ctx, zone, []libdns.Record{
ID: "foobar",
{
ID: "foobar",
},
})
// no matter which provider you use, the code stays the same!
@ -56,11 +60,11 @@ deletedRecs, err := provider.DeleteRecords(ctx, zone, []libdns.Record{
```
## Implementing new providers
## Implementing new provider packages
Providers are 100% written and maintained by the community! We all maintain just the packages for providers we use.
Provider packages are 100% written and maintained by the community! Collectively, we all maintain the packages for providers we individually use.
**[Instructions for adding new providers](https://github.com/libdns/libdns/wiki/Implementing-providers)** are on this repo's wiki. Please feel free to contribute.
**[Instructions for adding new libdns packages](https://github.com/libdns/libdns/wiki/Implementing-a-libdns-package)** are on this repo's wiki. Please feel free to contribute yours!
## Similar projects

View File

@ -10,15 +10,18 @@
// that input records conform to this standard, while also ensuring that
// output records do; adjustments to record names may need to be made before
// or after provider API calls, for example, to maintain consistency with
// all other libdns provider implementations. Helper functions are available
// in this package to convert between relative and absolute names.
// all other libdns packages. Helper functions are available in this package
// to convert between relative and absolute names.
//
// Although zone names are a required input, libdns does not coerce any
// particular representation of DNS zones; only records. Since zone name and
// records are separate inputs in libdns interfaces, it is up to the caller
// to pair a zone's name with its records in a way that works for them.
//
// All interface implementations must be safe for concurrent/parallel use.
// All interface implementations must be safe for concurrent/parallel use,
// meaning 1) no data races, and 2) simultaneous method calls must result
// in either both their expected outcomes or an error.
//
// For example, if AppendRecords() is called at the same time and two API
// requests are made to the provider at the same time, the result of both
// requests must be visible after they both complete; if the provider does
@ -32,6 +35,8 @@ package libdns
import (
"context"
"fmt"
"strconv"
"strings"
"time"
)
@ -89,7 +94,23 @@ type RecordDeleter interface {
DeleteRecords(ctx context.Context, zone string, recs []Record) ([]Record, error)
}
// ZoneLister can list available DNS zones.
type ZoneLister interface {
// ListZones returns the list of available DNS zones for use by
// other libdns methods.
//
// Implementations must honor context cancellation and be safe for
// concurrent use.
ListZones(ctx context.Context) ([]Zone, error)
}
// Record is a generalized representation of a DNS record.
//
// The values of this struct should be free of zone-file-specific syntax,
// except if this struct's fields do not sufficiently represent all the
// fields of a certain record type; in that case, the remaining data for
// which there are not specific fields should be stored in the Value as
// it appears in the zone file.
type Record struct {
// provider-specific metadata
ID string
@ -101,7 +122,76 @@ type Record struct {
TTL time.Duration
// type-dependent record fields
Priority int // used by MX, SRV, and URI records
Priority uint // HTTPS, MX, SRV, and URI records
Weight uint // SRV and URI records
}
// Zone is a generalized representation of a DNS zone.
type Zone struct {
Name string
}
// ToSRV parses the record into a SRV struct with fully-parsed, literal values.
//
// EXPERIMENTAL; subject to change or removal.
func (r Record) ToSRV() (SRV, error) {
if r.Type != "SRV" {
return SRV{}, fmt.Errorf("record type not SRV: %s", r.Type)
}
fields := strings.Fields(r.Value)
if len(fields) != 2 {
return SRV{}, fmt.Errorf("malformed SRV value; expected: '<port> <target>'")
}
port, err := strconv.Atoi(fields[0])
if err != nil {
return SRV{}, fmt.Errorf("invalid port %s: %v", fields[0], err)
}
if port < 0 {
return SRV{}, fmt.Errorf("port cannot be < 0: %d", port)
}
parts := strings.SplitN(r.Name, ".", 3)
if len(parts) < 3 {
return SRV{}, fmt.Errorf("name %v does not contain enough fields; expected format: '_service._proto.name'", r.Name)
}
return SRV{
Service: strings.TrimPrefix(parts[0], "_"),
Proto: strings.TrimPrefix(parts[1], "_"),
Name: parts[2],
Priority: r.Priority,
Weight: r.Weight,
Port: uint(port),
Target: fields[1],
}, nil
}
// SRV contains all the parsed data of an SRV record.
//
// EXPERIMENTAL; subject to change or removal.
type SRV struct {
Service string // no leading "_"
Proto string // no leading "_"
Name string
Priority uint
Weight uint
Port uint
Target string
}
// ToRecord converts the parsed SRV data to a Record struct.
//
// EXPERIMENTAL; subject to change or removal.
func (s SRV) ToRecord() Record {
return Record{
Type: "SRV",
Name: fmt.Sprintf("_%s._%s.%s", s.Service, s.Proto, s.Name),
Priority: s.Priority,
Weight: s.Weight,
Value: fmt.Sprintf("%d %s", s.Port, s.Target),
}
}
// RelativeName makes fqdn relative to zone. For example, for a FQDN of
@ -109,7 +199,13 @@ type Record struct {
//
// If fqdn cannot be expressed relative to zone, the input fqdn is returned.
func RelativeName(fqdn, zone string) string {
return strings.TrimSuffix(strings.TrimSuffix(fqdn, zone), ".")
// liberally ignore trailing dots on both fqdn and zone, because
// the relative name won't have a trailing dot anyway; I assume
// this won't be problematic...?
// (initially implemented because Cloudflare returns "fully-
// qualified" domains in their records without a trailing dot,
// but the input zone typically has a trailing dot)
return strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(fqdn, "."), strings.TrimSuffix(zone, ".")), ".")
}
// AbsoluteName makes name into a fully-qualified domain name (FQDN) by

8
vendor/modules.txt vendored
View File

@ -1,6 +1,6 @@
# github.com/libdns/cloudflare v0.1.0
## explicit; go 1.14
# github.com/libdns/cloudflare v0.1.1
## explicit; go 1.18
github.com/libdns/cloudflare
# github.com/libdns/libdns v0.2.1
## explicit; go 1.14
# github.com/libdns/libdns v0.2.2
## explicit; go 1.18
github.com/libdns/libdns