Compare commits

...

12 Commits
v0.1.0 ... main

15 changed files with 992 additions and 27 deletions

View File

@ -2,13 +2,26 @@ CLI tool to manage cloudflare DNS records
## Installation ## Installation
### Using [go](https://go.dev)
```bash ```bash
go install go.balki.me/cloudflare-dns-cli@latest go install go.balki.me/cloudflare-dns-cli@latest
# Run directly without installing
go run go.balki.me/cloudflare-dns-cli@latest -h
```
### Using [docker](https://www.docker.com)
```bash
mkdir -p $HOME/bin
docker run --pull=always --rm -v "$HOME/bin:/op" golang sh -c "go install go.balki.me/cloudflare-dns-cli@latest && install -o $(id -u) -g $(id -g) /go/bin/cloudflare-dns-cli /op/"
# Run directly without installing
docker run --pull=always --rm golang go run go.balki.me/cloudflare-dns-cli@latest -h
``` ```
## Features ## Features
1. Add/Modify `A`, `AAAA`, `CNAME` records 1. Add/Modify `A`, `AAAA`, `CNAME`, `MX` records
2. List/Delete records 2. List/Delete records
3. Save all records in JSON format 3. Save all records in JSON format
@ -24,8 +37,9 @@ Usage of cloudflare-dns-cli:
Domain name, e.g. example.com. env var: DOMAIN Domain name, e.g. example.com. env var: DOMAIN
-i value -i value
IP address (default 127.0.0.1) IP address (default 127.0.0.1)
-m Set mx record with cname value
-o string -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 -s string
Subdomain, e.g. blog. Use @ for root (default "<UNSET>") Subdomain, e.g. blog. Use @ for root (default "<UNSET>")
-x Delete records of subdomain -x Delete records of subdomain
@ -33,7 +47,7 @@ Usage of cloudflare-dns-cli:
## Demo ## Demo
===> [HERE](./demo.cast) <=== ===> [HERE](https://gitea.balki.me/balki/cloudflare-dns-cli/src/branch/main/demo.cast) <===
## Similar ## Similar
* https://github.com/earlchew/cloudflare-cli * https://github.com/earlchew/cloudflare-cli
@ -42,6 +56,6 @@ Usage of cloudflare-dns-cli:
## Why another cli? ## Why another cli?
1. Keep it simple with minimum dependency and features 1. Keep it simple with minimum dependency and features
2. Uses [libdns](https://github.com/libdns) library maintained by [caddy](https://caddyserver.com) team 2. Uses [libdns](https://github.com/libdns) library maintained by [caddy](https://caddyserver.com) team.
libdns provides uniform api for many dns providers. Mostly `s/cloudflare/XYZ/` should work when moving to a different dns provider libdns provides uniform api for many dns providers. Mostly `s/cloudflare/XYZ/` should work when moving to a different dns provider

6
go.mod
View File

@ -1,8 +1,8 @@
module go.balki.me/cloudflare-dns-cli module go.balki.me/cloudflare-dns-cli
go 1.20 go 1.22.2
require ( require (
github.com/libdns/cloudflare v0.1.0 github.com/libdns/cloudflare v0.1.1
github.com/libdns/libdns v0.2.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.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
github.com/libdns/cloudflare v0.1.0/go.mod h1:a44IP6J1YH6nvcNl1PverfJviADgXUnsozR3a7vBKN8= github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=

59
main.go
View File

@ -23,6 +23,7 @@ var (
sub = "<UNSET>" sub = "<UNSET>"
path string path string
del = false del = false
mx = false
) )
func genRecord() (libdns.Record, error) { func genRecord() (libdns.Record, error) {
@ -30,11 +31,20 @@ func genRecord() (libdns.Record, error) {
ipv6 := ip.To16() ipv6 := ip.To16()
switch { switch {
case cname != "": case cname != "":
return libdns.Record{ if mx {
Type: "CNAME", return libdns.Record{
Name: sub, Type: "MX",
Value: cname, Name: sub,
}, nil Value: cname,
Priority: 10,
}, nil
} else {
return libdns.Record{
Type: "CNAME",
Name: sub,
Value: cname,
}, nil
}
case ipv4 != nil: case ipv4 != nil:
return libdns.Record{ return libdns.Record{
Type: "A", Type: "A",
@ -54,9 +64,9 @@ func genRecord() (libdns.Record, error) {
func selectRecordsToDelete(recs []libdns.Record) []libdns.Record { func selectRecordsToDelete(recs []libdns.Record) []libdns.Record {
name := func() string { name := func() string {
if sub == "@" { if sub == "@" {
return domain return ""
} }
return fmt.Sprintf("%s.%s", sub, domain) return sub
}() }()
recs = func() (result []libdns.Record) { recs = func() (result []libdns.Record) {
for _, r := range recs { for _, r := range recs {
@ -66,12 +76,16 @@ func selectRecordsToDelete(recs []libdns.Record) []libdns.Record {
} }
return 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 { for i, r := range recs {
fmt.Printf("%5d %6s %s\n", i, r.Type, r.Value) fmt.Printf("%5d %6s %s\n", i, r.Type, r.Value)
} }
var delRange string 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 { if _, err := fmt.Scanln(&delRange); err != nil {
log.Panicln(err) log.Panicln(err)
} }
@ -99,10 +113,21 @@ func main() {
flag.StringVar(&token, "a", token, "Cloudflare API Token. env var: CF_TOKEN") flag.StringVar(&token, "a", token, "Cloudflare API Token. env var: CF_TOKEN")
flag.TextVar(&ip, "i", ip, "IP address") flag.TextVar(&ip, "i", ip, "IP address")
flag.StringVar(&cname, "c", cname, "CNAME target") 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(&del, "x", del, "Delete records of subdomain")
flag.BoolVar(&mx, "m", mx, "Set mx record with cname value")
flag.Parse() flag.Parse()
if token == "" {
flag.Usage()
log.Panicln("empty cloudflare api token")
}
if domain == "" {
flag.Usage()
log.Panicln("empty domain name")
}
provider := cloudflare.Provider{APIToken: token} provider := cloudflare.Provider{APIToken: token}
zone := domain + "." zone := domain + "."
@ -145,14 +170,20 @@ func main() {
if err != nil { if err != nil {
log.Panicln(err) log.Panicln(err)
} }
data, err := json.Marshal(recs)
data, err := json.Marshal(recs)
if err != nil { if err != nil {
log.Panicln(err) log.Panicln(err)
} }
err = os.WriteFile(path, data, 0644)
if err != nil { if path == "-" {
log.Panicln(err) 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) { func TestParseIP(t *testing.T) {
var ip net.IP var ip net.IP
if err := ip.UnmarshalText([]byte("127.0.0.1")); err != nil { if err := ip.UnmarshalText([]byte("127.0.0.1")); err != nil {

21
vendor/github.com/libdns/cloudflare/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Matthew Holt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

25
vendor/github.com/libdns/cloudflare/README.md generated vendored Normal file
View File

@ -0,0 +1,25 @@
Cloudflare for `libdns`
=======================
[![godoc reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/libdns/cloudflare)
This package implements the [libdns interfaces](https://github.com/libdns/libdns) for [Cloudflare](https://www.cloudflare.com).
## Authenticating
This package supports API **token** authentication.
You will need to create a token with the following permissions:
- Zone / Zone / Read
- Zone / DNS / Edit
The first permission is needed to get the zone ID, and the second permission is obviously necessary to edit the DNS records. If you're only using the `GetRecords()` method, you can change the second permission to Read to guarantee no changes will be made.
To clarify, do NOT use API keys, which are globally-scoped:
![Don't use API keys](https://user-images.githubusercontent.com/1128849/81196485-556aca00-8f7c-11ea-9e13-c6a8a966f689.png)
DO use scoped API tokens:
![Don't use API keys](https://user-images.githubusercontent.com/1128849/81196503-5c91d800-8f7c-11ea-93cc-ad7d73420fab.png)

154
vendor/github.com/libdns/cloudflare/client.go generated vendored Normal file
View File

@ -0,0 +1,154 @@
package cloudflare
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/libdns/libdns"
)
func (p *Provider) createRecord(ctx context.Context, zoneInfo cfZone, record libdns.Record) (cfDNSRecord, error) {
cfRec, err := cloudflareRecord(record)
if err != nil {
return cfDNSRecord{}, err
}
jsonBytes, err := json.Marshal(cfRec)
if err != nil {
return cfDNSRecord{}, err
}
reqURL := fmt.Sprintf("%s/zones/%s/dns_records", baseURL, zoneInfo.ID)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewReader(jsonBytes))
if err != nil {
return cfDNSRecord{}, err
}
req.Header.Set("Content-Type", "application/json")
var result cfDNSRecord
_, err = p.doAPIRequest(req, &result)
if err != nil {
return cfDNSRecord{}, err
}
return result, nil
}
// updateRecord updates a DNS record. oldRec must have both an ID and zone ID.
// Only the non-empty fields in newRec will be changed.
func (p *Provider) updateRecord(ctx context.Context, oldRec, newRec cfDNSRecord) (cfDNSRecord, error) {
reqURL := fmt.Sprintf("%s/zones/%s/dns_records/%s", baseURL, oldRec.ZoneID, oldRec.ID)
jsonBytes, err := json.Marshal(newRec)
if err != nil {
return cfDNSRecord{}, err
}
// PATCH changes only the populated fields; PUT resets Type, Name, Content, and TTL even if empty
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, reqURL, bytes.NewReader(jsonBytes))
if err != nil {
return cfDNSRecord{}, err
}
req.Header.Set("Content-Type", "application/json")
var result cfDNSRecord
_, err = p.doAPIRequest(req, &result)
return result, err
}
func (p *Provider) getDNSRecords(ctx context.Context, zoneInfo cfZone, rec libdns.Record, matchContent bool) ([]cfDNSRecord, error) {
qs := make(url.Values)
qs.Set("type", rec.Type)
qs.Set("name", libdns.AbsoluteName(rec.Name, zoneInfo.Name))
if matchContent {
qs.Set("content", rec.Value)
}
reqURL := fmt.Sprintf("%s/zones/%s/dns_records?%s", baseURL, zoneInfo.ID, qs.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return nil, err
}
var results []cfDNSRecord
_, err = p.doAPIRequest(req, &results)
return results, err
}
func (p *Provider) getZoneInfo(ctx context.Context, zoneName string) (cfZone, error) {
p.zonesMu.Lock()
defer p.zonesMu.Unlock()
// if we already got the zone info, reuse it
if p.zones == nil {
p.zones = make(map[string]cfZone)
}
if zone, ok := p.zones[zoneName]; ok {
return zone, nil
}
qs := make(url.Values)
qs.Set("name", zoneName)
reqURL := fmt.Sprintf("%s/zones?%s", baseURL, qs.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return cfZone{}, err
}
var zones []cfZone
_, err = p.doAPIRequest(req, &zones)
if err != nil {
return cfZone{}, err
}
if len(zones) != 1 {
return cfZone{}, fmt.Errorf("expected 1 zone, got %d for %s", len(zones), zoneName)
}
// cache this zone for possible reuse
p.zones[zoneName] = zones[0]
return zones[0], nil
}
// doAPIRequest authenticates the request req and does the round trip. It returns
// the decoded response from Cloudflare if successful; otherwise it returns an
// error including error information from the API if applicable. If result is a
// non-nil pointer, the result field from the API response will be decoded into
// it for convenience.
func (p *Provider) doAPIRequest(req *http.Request, result interface{}) (cfResponse, error) {
req.Header.Set("Authorization", "Bearer "+p.APIToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return cfResponse{}, err
}
defer resp.Body.Close()
var respData cfResponse
err = json.NewDecoder(resp.Body).Decode(&respData)
if err != nil {
return cfResponse{}, err
}
if resp.StatusCode >= 400 {
return cfResponse{}, fmt.Errorf("got error status: HTTP %d: %+v", resp.StatusCode, respData.Errors)
}
if len(respData.Errors) > 0 {
return cfResponse{}, fmt.Errorf("got errors: HTTP %d: %+v", resp.StatusCode, respData.Errors)
}
if len(respData.Result) > 0 && result != nil {
err = json.Unmarshal(respData.Result, result)
if err != nil {
return cfResponse{}, err
}
respData.Result = nil
}
return respData, err
}
const baseURL = "https://api.cloudflare.com/client/v4"

188
vendor/github.com/libdns/cloudflare/models.go generated vendored Normal file
View File

@ -0,0 +1,188 @@
package cloudflare
import (
"encoding/json"
"strings"
"time"
"github.com/libdns/libdns"
)
type cfZone struct {
ID string `json:"id"`
Name string `json:"name"`
DevelopmentMode int `json:"development_mode"`
OriginalNameServers []string `json:"original_name_servers"`
OriginalRegistrar string `json:"original_registrar"`
OriginalDnshost string `json:"original_dnshost"`
CreatedOn time.Time `json:"created_on"`
ModifiedOn time.Time `json:"modified_on"`
ActivatedOn time.Time `json:"activated_on"`
Account struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"account"`
Permissions []string `json:"permissions"`
Plan struct {
ID string `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
Currency string `json:"currency"`
Frequency string `json:"frequency"`
LegacyID string `json:"legacy_id"`
IsSubscribed bool `json:"is_subscribed"`
CanSubscribe bool `json:"can_subscribe"`
} `json:"plan"`
PlanPending struct {
ID string `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
Currency string `json:"currency"`
Frequency string `json:"frequency"`
LegacyID string `json:"legacy_id"`
IsSubscribed bool `json:"is_subscribed"`
CanSubscribe bool `json:"can_subscribe"`
} `json:"plan_pending"`
Status string `json:"status"`
Paused bool `json:"paused"`
Type string `json:"type"`
NameServers []string `json:"name_servers"`
}
type cfDNSRecord struct {
ID string `json:"id,omitempty"`
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Content string `json:"content,omitempty"`
Proxiable bool `json:"proxiable,omitempty"`
Proxied bool `json:"proxied,omitempty"`
TTL int `json:"ttl,omitempty"` // seconds
Locked bool `json:"locked,omitempty"`
ZoneID string `json:"zone_id,omitempty"`
ZoneName string `json:"zone_name,omitempty"`
CreatedOn time.Time `json:"created_on,omitempty"`
ModifiedOn time.Time `json:"modified_on,omitempty"`
Priority uint `json:"priority,omitempty"`
Data struct {
// LOC
LatDegrees int `json:"lat_degrees,omitempty"`
LatMinutes int `json:"lat_minutes,omitempty"`
LatSeconds int `json:"lat_seconds,omitempty"`
LatDirection string `json:"lat_direction,omitempty"`
LongDegrees int `json:"long_degrees,omitempty"`
LongMinutes int `json:"long_minutes,omitempty"`
LongSeconds int `json:"long_seconds,omitempty"`
LongDirection string `json:"long_direction,omitempty"`
Altitude int `json:"altitude,omitempty"`
Size int `json:"size,omitempty"`
PrecisionHorz int `json:"precision_horz,omitempty"`
PrecisionVert int `json:"precision_vert,omitempty"`
// SRV
Service string `json:"service,omitempty"`
Proto string `json:"proto,omitempty"`
Name string `json:"name,omitempty"`
Priority uint `json:"priority,omitempty"`
Weight uint `json:"weight,omitempty"`
Port uint `json:"port,omitempty"`
Target string `json:"target,omitempty"`
// DNSKEY
Flags int `json:"flags,omitempty"`
Protocol int `json:"protocol,omitempty"`
Algorithm int `json:"algorithm,omitempty"`
// DS
KeyTag int `json:"key_tag,omitempty"`
DigestType int `json:"digest_type,omitempty"`
// TLSA
Usage int `json:"usage,omitempty"`
Selector int `json:"selector,omitempty"`
MatchingType int `json:"matching_type,omitempty"`
// URI
Content string `json:"content,omitempty"`
} `json:"data,omitempty"`
Meta *struct {
AutoAdded bool `json:"auto_added,omitempty"`
Source string `json:"source,omitempty"`
} `json:"meta,omitempty"`
}
func (r cfDNSRecord) libdnsRecord(zone string) 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, 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.
type cfResponse struct {
Result json.RawMessage `json:"result,omitempty"`
Success bool `json:"success"`
Errors []struct {
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 []any `json:"messages,omitempty"`
ResultInfo *cfResultInfo `json:"result_info,omitempty"`
}
type cfResultInfo struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Count int `json:"count"`
TotalCount int `json:"total_count"`
}

181
vendor/github.com/libdns/cloudflare/provider.go generated vendored Normal file
View File

@ -0,0 +1,181 @@
package cloudflare
import (
"context"
"fmt"
"net/http"
"sync"
"github.com/libdns/libdns"
)
// Provider implements the libdns interfaces for Cloudflare.
// TODO: Support pagination and retries, handle rate limits.
type Provider struct {
// API token is used for authentication. Make sure to use a
// scoped API **token**, NOT a global API **key**. It will
// need two permissions: Zone-Zone-Read and Zone-DNS-Edit,
// unless you are only using `GetRecords()`, in which case
// the second can be changed to Read.
APIToken string `json:"api_token,omitempty"`
zones map[string]cfZone
zonesMu sync.Mutex
}
// GetRecords lists all the records in the zone.
func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
zoneInfo, err := p.getZoneInfo(ctx, zone)
if err != nil {
return nil, err
}
reqURL := fmt.Sprintf("%s/zones/%s/dns_records", baseURL, zoneInfo.ID)
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
if err != nil {
return nil, err
}
var result []cfDNSRecord
_, err = p.doAPIRequest(req, &result)
if err != nil {
return nil, err
}
recs := make([]libdns.Record, 0, len(result))
for _, rec := range result {
recs = append(recs, rec.libdnsRecord(zone))
}
return recs, nil
}
// AppendRecords adds records to the zone. It returns the records that were added.
func (p *Provider) AppendRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
zoneInfo, err := p.getZoneInfo(ctx, zone)
if err != nil {
return nil, err
}
var created []libdns.Record
for _, rec := range records {
result, err := p.createRecord(ctx, zoneInfo, rec)
if err != nil {
return nil, err
}
created = append(created, result.libdnsRecord(zone))
}
return created, nil
}
// DeleteRecords deletes the records from the zone. If a record does not have an ID,
// it will be looked up. It returns the records that were deleted.
func (p *Provider) DeleteRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
zoneInfo, err := p.getZoneInfo(ctx, zone)
if err != nil {
return nil, err
}
var recs []libdns.Record
for _, rec := range records {
// we create a "delete queue" for each record
// requested for deletion; if the record ID
// is known, that is the only one to fill the
// queue, but if it's not known, we try to find
// a match theoretically there could be more
// than one
var deleteQueue []libdns.Record
if rec.ID == "" {
// record ID is required; try to find it with what was provided
exactMatches, err := p.getDNSRecords(ctx, zoneInfo, rec, true)
if err != nil {
return nil, err
}
for _, rec := range exactMatches {
deleteQueue = append(deleteQueue, rec.libdnsRecord(zone))
}
} else {
deleteQueue = []libdns.Record{rec}
}
for _, delRec := range deleteQueue {
reqURL := fmt.Sprintf("%s/zones/%s/dns_records/%s", baseURL, zoneInfo.ID, delRec.ID)
req, err := http.NewRequestWithContext(ctx, "DELETE", reqURL, nil)
if err != nil {
return nil, err
}
var result cfDNSRecord
_, err = p.doAPIRequest(req, &result)
if err != nil {
return nil, err
}
recs = append(recs, result.libdnsRecord(zone))
}
}
return recs, nil
}
// SetRecords sets the records in the zone, either by updating existing records
// or creating new ones. It returns the updated records.
func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
zoneInfo, err := p.getZoneInfo(ctx, zone)
if err != nil {
return nil, err
}
var results []libdns.Record
for _, rec := range records {
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
matches, err := p.getDNSRecords(ctx, zoneInfo, rec, false)
if err != nil {
return nil, err
}
if len(matches) == 0 {
// record doesn't exist; create it
result, err := p.createRecord(ctx, zoneInfo, rec)
if err != nil {
return nil, err
}
results = append(results, result.libdnsRecord(zone))
continue
}
if len(matches) > 1 {
return nil, fmt.Errorf("unexpectedly found more than 1 record for %v", rec)
}
// record does exist, fill in the ID so that we can update it
oldRec.ID = matches[0].ID
}
// record exists; update it
cfRec, err := cloudflareRecord(rec)
if err != nil {
return nil, err
}
result, err := p.updateRecord(ctx, oldRec, cfRec)
if err != nil {
return nil, err
}
results = append(results, result.libdnsRecord(zone))
}
return results, nil
}
// Interface guards
var (
_ libdns.RecordGetter = (*Provider)(nil)
_ libdns.RecordAppender = (*Provider)(nil)
_ libdns.RecordSetter = (*Provider)(nil)
_ libdns.RecordDeleter = (*Provider)(nil)
)

1
vendor/github.com/libdns/libdns/.gitignore generated vendored Normal file
View File

@ -0,0 +1 @@
_gitignore/

21
vendor/github.com/libdns/libdns/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Matthew Holt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

91
vendor/github.com/libdns/libdns/README.md generated vendored Normal file
View File

@ -0,0 +1,91 @@
libdns - Universal DNS provider APIs for Go
===========================================
<a href="https://pkg.go.dev/github.com/libdns/libdns"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
**⚠️ Work-in-progress. Exported APIs are subject to change.**
`libdns` is a collection of free-range DNS provider client implementations written in Go! With libdns packages, your Go program can manage DNS records across any supported providers. A "provider" is a service or program that manages a DNS zone.
This repository defines the core interfaces that provider packages should implement. They are small and idiomatic Go interfaces with well-defined semantics.
The interfaces include:
- [`RecordGetter`](https://pkg.go.dev/github.com/libdns/libdns#RecordGetter) to list records.
- [`RecordAppender`](https://pkg.go.dev/github.com/libdns/libdns#RecordAppender) to append new records.
- [`RecordSetter`](https://pkg.go.dev/github.com/libdns/libdns#RecordSetter) to set (create or change existing) records.
- [`RecordDeleter`](https://pkg.go.dev/github.com/libdns/libdns#RecordDeleter) to delete records.
[See full godoc for detailed documentation.](https://pkg.go.dev/github.com/libdns/libdns)
## Example
To work with DNS records managed by Cloudflare, for example, we can use [libdns/cloudflare](https://pkg.go.dev/github.com/libdns/cloudflare):
```go
import (
"github.com/libdns/cloudflare"
"github.com/libdns/libdns"
)
ctx := context.TODO()
zone := "example.com."
// configure the DNS provider (choose any from github.com/libdns)
provider := cloudflare.Provider{APIToken: "topsecret"}
// list records
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",
},
})
// delete records (this example uses provider-assigned ID)
deletedRecs, err := provider.DeleteRecords(ctx, zone, []libdns.Record{
{
ID: "foobar",
},
})
// no matter which provider you use, the code stays the same!
// (some providers have caveats; see their package documentation)
```
## Implementing new provider packages
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 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
**[OctoDNS](https://github.com/github/octodns)** is a suite of tools written in Python for managing DNS. However, its approach is a bit heavy-handed when all you need are small, incremental changes to a zone:
> WARNING: OctoDNS assumes ownership of any domain you point it to. When you tell it to act it will do whatever is necessary to try and match up states including deleting any unexpected records. Be careful when playing around with OctoDNS.
This is incredibly useful when you are maintaining your own zone file, but risky when you just need incremental changes.
**[StackExchange/dnscontrol](https://github.com/StackExchange/dnscontrol)** is written in Go, but is similar to OctoDNS in that it tends to obliterate your entire zone and replace it with your input. Again, this is very useful if you are maintaining your own master list of records, but doesn't do well for simply adding or removing records.
**[go-acme/lego](https://github.com/go-acme/lego)** has support for a huge number of DNS providers (75+!), but their APIs are only capable of setting and deleting TXT records for ACME challenges.
**`libdns`** takes inspiration from the above projects but aims for a more generally-useful set of APIs that homogenize pretty well across providers. In contrast to the above projects, libdns can add, set, delete, and get arbitrary records from a zone without obliterating it (although syncing up an entire zone is also possible!). Its APIs also include context so long-running calls can be cancelled early, for example to accommodate on-line config changes downstream. libdns interfaces are also smaller and more composable. Additionally, libdns can grow to support a nearly infinite number of DNS providers without added bloat, because each provider implementation is a separate Go module, which keeps your builds lean and fast.
In summary, the goal is that libdns providers can do what the above libraries/tools can do, but with more flexibility: they can create and delete TXT records for ACME challenges, they can replace entire zones, but they can also do incremental changes or simply read records.
## Record abstraction
How records are represented across providers varies widely, and each kind of record has different fields and semantics. In time, our goal is for the `libdns.Record` type to be able to represent most of them as concisely and simply as possible, with the interface methods able to deliver on most of the possible zone operations.
Realistically, libdns should enable most common record manipulations, but may not be able to fit absolutely 100% of all possibilities with DNS in a provider-agnostic way. That is probably OK; and given the wide varieties in DNS record types and provider APIs, it would be unreasonable to expect otherwise. We are not aiming for 100% fulfillment of 100% of users' requirements; more like 100% fulfillment of ~90% of users' requirements.

225
vendor/github.com/libdns/libdns/libdns.go generated vendored Normal file
View File

@ -0,0 +1,225 @@
// Package libdns defines core interfaces that should be implemented by DNS
// provider clients. They are small and idiomatic Go interfaces with
// well-defined semantics.
//
// Records are described independently of any particular zone, a convention
// that grants Record structs portability across zones. As such, record names
// are partially qualified, i.e. relative to the zone. For example, an A
// record called "sub" in zone "example.com." represents a fully-qualified
// domain name (FQDN) of "sub.example.com.". Implementations should expect
// 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 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,
// 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
// not synchronize the writing of the zone file and one request overwrites
// the other, then the client implementation must take care to synchronize
// on behalf of the incompetent provider. This synchronization need not be
// global; for example: the scope of synchronization might only need to be
// within the same zone, allowing multiple requests at once as long as all
// of them are for different zones. (Exact logic depends on the provider.)
package libdns
import (
"context"
"fmt"
"strconv"
"strings"
"time"
)
// RecordGetter can get records from a DNS zone.
type RecordGetter interface {
// GetRecords returns all the records in the DNS zone.
//
// Implementations must honor context cancellation and be safe for
// concurrent use.
GetRecords(ctx context.Context, zone string) ([]Record, error)
}
// RecordAppender can non-destructively add new records to a DNS zone.
type RecordAppender interface {
// AppendRecords creates the requested records in the given zone
// and returns the populated records that were created. It never
// changes existing records.
//
// Implementations must honor context cancellation and be safe for
// concurrent use.
AppendRecords(ctx context.Context, zone string, recs []Record) ([]Record, error)
}
// RecordSetter can set new or update existing records in a DNS zone.
type RecordSetter interface {
// SetRecords updates the zone so that the records described in the
// input are reflected in the output. It may create or overwrite
// records or -- depending on the record type -- delete records to
// maintain parity with the input. No other records are affected.
// It returns the records which were set.
//
// Records that have an ID associating it with a particular resource
// on the provider will be directly replaced. If no ID is given, this
// method may use what information is given to do lookups and will
// ensure that only necessary changes are made to the zone.
//
// Implementations must honor context cancellation and be safe for
// concurrent use.
SetRecords(ctx context.Context, zone string, recs []Record) ([]Record, error)
}
// RecordDeleter can delete records from a DNS zone.
type RecordDeleter interface {
// DeleteRecords deletes the given records from the zone if they exist.
// It returns the records that were deleted.
//
// Records that have an ID to associate it with a particular resource on
// the provider will be directly deleted. If no ID is given, this method
// may use what information is given to do lookups and delete only
// matching records.
//
// Implementations must honor context cancellation and be safe for
// concurrent use.
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
// general record fields
Type string
Name string // partially-qualified (relative to zone)
Value string
TTL time.Duration
// type-dependent record fields
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
// "sub.example.com" and a zone of "example.com", it outputs "sub".
//
// If fqdn cannot be expressed relative to zone, the input fqdn is returned.
func RelativeName(fqdn, zone string) string {
// 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
// prepending it to zone and tidying up the dots. For example, an input
// of name "sub" and zone "example.com." will return "sub.example.com.".
func AbsoluteName(name, zone string) string {
if zone == "" {
return strings.Trim(name, ".")
}
if name == "" || name == "@" {
return zone
}
if !strings.HasSuffix(name, ".") {
name += "."
}
return name + zone
}

6
vendor/modules.txt vendored Normal file
View File

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