pdns-auth-proxy/pdns.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
}