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 }