321 lines
8.4 KiB
Go
321 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// PowerDNS defines the client to which queries are passed
|
|
type (
|
|
PowerDNS struct {
|
|
Scheme string
|
|
Hostname string
|
|
Port string
|
|
apiURL string
|
|
debug bool
|
|
Client *http.Client
|
|
m sync.RWMutex
|
|
listCache map[string]*listCache
|
|
DefaultTTL int
|
|
}
|
|
)
|
|
|
|
// NewClient initializes a new PowerDNS client configuration
|
|
func NewClient(baseURL, apiKey string, timeout, ttl int) (*PowerDNS, error) {
|
|
scheme, hostname, port, path, err := parseBaseURL(baseURL)
|
|
if err != nil {
|
|
log.Fatalf("%s is not a valid URL: %v", baseURL, err)
|
|
}
|
|
transport := http.DefaultTransport
|
|
apiKeyTransport := &APIKeyTransport{
|
|
Transport: transport,
|
|
APIKey: apiKey,
|
|
}
|
|
errorTransport := &ErrorTransport{
|
|
Transport: apiKeyTransport,
|
|
}
|
|
powerDNS := &PowerDNS{
|
|
Scheme: scheme,
|
|
Hostname: hostname,
|
|
Port: port,
|
|
Client: &http.Client{
|
|
Transport: errorTransport,
|
|
Timeout: time.Duration(timeout) * time.Second,
|
|
},
|
|
apiURL: path,
|
|
listCache: map[string]*listCache{},
|
|
DefaultTTL: ttl,
|
|
}
|
|
return powerDNS, nil
|
|
}
|
|
|
|
// Debug toggle the debug mode
|
|
func (p *PowerDNS) Debug() {
|
|
p.debug = true
|
|
}
|
|
|
|
// LogDebug facility
|
|
func (p *PowerDNS) LogDebug(v ...interface{}) {
|
|
if p.debug {
|
|
log.Println(v...)
|
|
}
|
|
}
|
|
|
|
// Lock the structure
|
|
func (p *PowerDNS) lock() {
|
|
p.m.Lock()
|
|
}
|
|
|
|
// ...or unlock it
|
|
func (p *PowerDNS) unlock() {
|
|
p.m.Unlock()
|
|
}
|
|
|
|
// Ping tries to contact a powerDNS URL API to make sure its up and accessible
|
|
func (p *PowerDNS) Ping(ctx context.Context) error {
|
|
u := url.URL{}
|
|
u.Host = p.Hostname + ":" + p.Port
|
|
u.Scheme = p.Scheme
|
|
u.Path = "/api"
|
|
req, err := http.NewRequest("GET", u.String(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := p.Client.Do(req.WithContext(ctx))
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Execute send the DNSQuery structure to the PDNS api
|
|
func (p *PowerDNS) Execute(d *DNSQuery) (int, http.Header, error) {
|
|
uri := fmt.Sprintf("%s/%s/%s", p.apiURL, "zones", trimPoint(d.Domain))
|
|
return p.sendQuery(context.Background(), uri, "PATCH", printJSON(d), nil)
|
|
}
|
|
|
|
// ExecuteZone send the DNSZone structure to the PDNS api
|
|
func (p *PowerDNS) ExecuteZone(d *DNSZone, resp *DNSQuery) (int, http.Header, error) {
|
|
uri := fmt.Sprintf("%s/%s", p.apiURL, "zones")
|
|
// purge the zone cache
|
|
p.listCache = map[string]*listCache{}
|
|
return p.sendQuery(context.Background(), uri, "POST", printJSON(d), resp)
|
|
}
|
|
|
|
// Zone return the whole zone if it exist
|
|
func (p *PowerDNS) Zone(name string) (*DNSQuery, error) {
|
|
var result DNSQuery
|
|
|
|
parentName, err := p.GetDomain(name)
|
|
if err != nil {
|
|
return nil, errors.New("Unknown domain")
|
|
}
|
|
code, _, err := p.sendQuery(context.Background(),
|
|
fmt.Sprintf("%s/%s/%s", p.apiURL, "zones", parentName), "GET", nil, &result)
|
|
if code != 200 {
|
|
return nil, errors.New("Unknown zone, but it shouldn't")
|
|
}
|
|
// if this is a subzone, filter the result
|
|
if strings.HasSuffix(name, "."+addPoint(parentName)) {
|
|
good := []int{}
|
|
filter := []*DNSRRSet{}
|
|
sub := "." + name
|
|
for i := range result.RRSets {
|
|
if result.RRSets[i].Name == name || strings.HasSuffix(result.RRSets[i].Name, sub) {
|
|
good = append(good, i)
|
|
}
|
|
}
|
|
for _, i := range good {
|
|
filter = append(filter, result.RRSets[i])
|
|
}
|
|
result.RRSets = filter
|
|
}
|
|
if err == nil {
|
|
result.Domain = name
|
|
}
|
|
return &result, err
|
|
}
|
|
|
|
// Search perform a search in the pdns API
|
|
func (p *PowerDNS) Search(r string) (DNSSearch, error) {
|
|
var records DNSSearch
|
|
// remove any final point to the record if necessary
|
|
url := fmt.Sprintf("%s/%s?max=5000&q=%s", p.apiURL, "search-data", trimPoint(r))
|
|
|
|
if _, _, err := p.sendQuery(context.Background(), url, "GET", nil, &records); err != nil {
|
|
return nil, err
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
// IsUsed search the DB to check if anyting is pointing to name
|
|
func (p *PowerDNS) IsUsed(name, exception string, rtype []string) bool {
|
|
exception = strings.ToLower(exception)
|
|
search, err := p.Search(name)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, rec := range search {
|
|
if !rec.IsRecord() || rec.Name == exception {
|
|
continue
|
|
}
|
|
for _, t := range rtype {
|
|
if t == rec.Type {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ReverseChanges returns the list of changes needed if we move the value of
|
|
// the record d which pointed to excludeIP
|
|
func (p *PowerDNS) ReverseChanges(d *DNSQuery, excludeIP string) ([]*DNSQuery, error) {
|
|
actions := []*DNSQuery{}
|
|
for _, entry := range d.RRSets {
|
|
for _, record := range entry.Records {
|
|
ip := record.Content
|
|
// ignore the record pointing to excludeIP
|
|
if ip == excludeIP {
|
|
continue
|
|
}
|
|
reverse, _ := p.GetReverse(ip, entry.Type == "A")
|
|
// no reverse (inconstancy or external IP): no problem here
|
|
if reverse == nil || reverse.Len() == 0 {
|
|
continue
|
|
}
|
|
// this reverse doesn't point back to entry.Name, no action needed
|
|
if !reverse.RemoveValue(entry.Name) {
|
|
continue
|
|
}
|
|
// if we don't remove the reverse, no problem
|
|
if !reverse.RRSets[0].IsDeletion() {
|
|
continue
|
|
}
|
|
// there was only one PTR, and it's pointing back to j.Params.Name, we
|
|
// need to remove it
|
|
if p.IsUsed(ip, entry.Name, []string{entry.Type}) {
|
|
message := "Reverse issue : %s is the reverse for %s and will be changed\n"
|
|
message += "But other records are pointing to %s as well. Please cleanup first\n"
|
|
return actions, fmt.Errorf(message, entry.Name, ip, ip)
|
|
}
|
|
actions = append(actions, reverse)
|
|
}
|
|
}
|
|
return actions, nil
|
|
}
|
|
|
|
// getRecord get the PDNS record
|
|
func (p *PowerDNS) getRecord(r string, wanted, notWanted []string, ignoreBadDomain bool) (*DNSQuery, error) {
|
|
// Get the domain
|
|
d, err := p.GetDomain(r)
|
|
|
|
// we don't manage the domain, no need to continue (ignoreBadDomain is used
|
|
// for newly created domains only)
|
|
if err != nil && !ignoreBadDomain {
|
|
return nil, err
|
|
}
|
|
// get the records, in search format
|
|
records, err := p.Search(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Now we convert the search format to the DNSQuery format
|
|
query := records.DNSQuery()
|
|
query.Domain = d
|
|
|
|
// and we do some cleanup
|
|
err = query.RecordFilter(r, wanted, notWanted)
|
|
|
|
return query, err
|
|
}
|
|
|
|
// CanCreate return an error if t can be created on the local DNS and doesn't
|
|
// exist. An external record, existing or not, is fine
|
|
func (p *PowerDNS) CanCreate(r string, directOnly bool, target *DNSQuery) error {
|
|
search, err := p.getRecord(r, []string{"*"}, []string{}, false)
|
|
// copy the actual structure
|
|
if target != nil && search != nil {
|
|
*target = *search
|
|
}
|
|
switch {
|
|
case err == nil:
|
|
break
|
|
case err.Error() == "Unknown domain":
|
|
return nil
|
|
case strings.HasSuffix(err.Error(), "delegated to another server"):
|
|
return nil
|
|
case err != nil:
|
|
return err
|
|
}
|
|
if directOnly {
|
|
if err := search.RecordFilter(r,
|
|
[]string{"A", "AAAA"},
|
|
[]string{"CNAME", "DNAME"}); err != nil {
|
|
return fmt.Errorf("%s cannot be a CNAME", r)
|
|
}
|
|
}
|
|
if search.Len() == 0 {
|
|
return fmt.Errorf("%s doesn't exist, create it first", r)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetRecord get the PDNS record of type t
|
|
func (p *PowerDNS) GetRecord(r, t string, ignoreBadDomain bool) (*DNSQuery, error) {
|
|
notWanted := []string{}
|
|
wanted := []string{strings.ToUpper(t)}
|
|
switch t {
|
|
case "ttl":
|
|
wanted = []string{"*"}
|
|
notWanted = []string{}
|
|
case "delete":
|
|
wanted = []string{"*", "indirect"}
|
|
case "cname":
|
|
notWanted = []string{"*"}
|
|
case "dname":
|
|
notWanted = []string{"*"}
|
|
case "ns":
|
|
wanted = []string{"NS"}
|
|
notWanted = []string{"*"}
|
|
default:
|
|
notWanted = []string{"CNAME", "DNAME"}
|
|
}
|
|
return p.getRecord(r, wanted, notWanted, ignoreBadDomain)
|
|
}
|
|
|
|
// GetReverse get the reverse DNS record of the IP
|
|
func (p *PowerDNS) GetReverse(ipString string, v4 bool) (*DNSQuery, error) {
|
|
ip := net.ParseIP(ipString)
|
|
if ip == nil || (v4 != (ip.To4() != nil)) {
|
|
return nil, errors.New("not a valid IP address")
|
|
}
|
|
// we ignore the error on the actual query, which most certainly means
|
|
// there is no reverse or that we don't manage it
|
|
ret, _ := p.getRecord(iPtoReverse(ip), []string{"PTR"}, nil, false)
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// SetNameServers returns a *DNSQuery to create a set of nameservers in a zone
|
|
// or its parent
|
|
func (p *PowerDNS) SetNameServers(zone, where string, servers []string) (*DNSQuery, error) {
|
|
ns, err := p.GetRecord(zone, "NS", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, nameServer := range servers {
|
|
ns.ChangeValue(zone, nameServer, "NS", false, false)
|
|
}
|
|
ns.ChangeDomain(where)
|
|
return ns, nil
|
|
}
|