pdns-auth-proxy/pdns_query.go

468 lines
12 KiB
Go

package main
import (
"fmt"
"sort"
"strconv"
"strings"
)
type (
// DNSQuery is the json structure of the get/add/replace actions in the PDNS API
DNSQuery struct {
RRSets []*DNSRRSet `json:"rrsets"`
Domain string `json:"domain"`
}
// DNSRRSet is the json structure of a DNS entry in the PDNS API
DNSRRSet struct {
Name string `json:"name"`
Type string `json:"type"`
Comment []*DNSComments `json:"comments"`
Records []*DNSRecord `json:"records"`
ChangeType string `json:"changetype"`
TTL int `json:"ttl"`
PlainText string `json:"-"`
}
// DNSComments is the json structure of a comment in the PDNS API
DNSComments struct {
Account string `json:"account"`
Content string `json:"content"`
}
// DNSRecord is the json structure of a record in the PDNS API
DNSRecord struct {
Content string `json:"content"`
Disabled bool `json:"disabled"`
SetPTR bool `json:"set-ptr"`
}
)
// IsDeletion tells if the nth record of the query is a DELETE or not
func (d *DNSQuery) IsDeletion(n int) bool {
if d.RecordSize(n) < n {
return false
}
return d.RRSets[0].IsDeletion()
}
// ChangeDomain sets a new name for the DNSQuery
func (d *DNSQuery) ChangeDomain(s string) {
d.Domain = s
}
// HasDomain tell of the query as the Domain variable empty or not
func (d *DNSQuery) HasDomain() bool {
return len(d.Domain) != 0
}
// SetPlainTexts add relevent PlainTexts to the RRSets
func (d *DNSQuery) SetPlainTexts(action string, reverse bool) {
for n := range d.RRSets {
i := d.RecordSize(n) - 1
if i < 0 {
return
}
if d.RRSets[n].ChangeType == "" {
d.RRSets[n].ChangeType = "REPLACE"
}
comment := fmt.Sprintf("%s %s record %s to %s",
action, d.RRSets[n].Type, d.RRSets[n].Name, d.RRSets[n].Records[i].Content)
if reverse {
comment += " and add the reverse"
}
d.AddPlainText(n, comment)
}
}
// ChangeValue adds a new value to the first record of the query, and creates it
// if necessary
func (d *DNSQuery) ChangeValue(name, value, rtype string, overwrite, reverse bool) {
rtype = strings.ToUpper(rtype)
name = strings.ToLower(name)
if rtype != "TXT" {
value = strings.ToLower(value)
}
if rtype != "A" && rtype != "AAAA" {
reverse = false
}
action := "Append a new"
switch {
case len(d.RRSets) == 0:
action = "Create a new"
case overwrite:
action = "Modify"
}
if overwrite || d.Len() == 0 {
d.RRSets = []*DNSRRSet{{
Name: name,
Type: rtype,
Records: []*DNSRecord{},
ChangeType: "REPLACE",
}}
}
d.RRSets[0].Records = append(d.RRSets[0].Records, &DNSRecord{
Content: value,
Disabled: false,
SetPTR: reverse,
})
d.SetPlainTexts(action, reverse)
}
// RecordFilter cleans a []*DNSRRSet by keeping only wanted elements
func (d *DNSQuery) RecordFilter(r string, wanted, notWanted []string) error {
good := []int{}
// Add a final point to the record if necessary
r = addPoint(r)
for i, record := range d.RRSets {
switch {
// ignore reverse & co by default
case record.Name != r && !stringInSlice("indirect", wanted):
continue
// check for forbidden types
case stringInSlice(record.Type, notWanted):
return fmt.Errorf("This entry is a %s, you must delete it first", record.Type)
// this is the wanted type
case stringInSlice(record.Type, wanted):
good = append(good, i)
// this is too
case stringInSlice("*", wanted):
good = append(good, i)
// in certain case, we don't want any entry type
case stringInSlice("*", notWanted):
return fmt.Errorf("This entry is a %s, you must delete it first", record.Type)
}
}
ret := []*DNSRRSet{}
for _, i := range good {
ret = append(ret, d.RRSets[i])
}
d.RRSets = ret
return nil
}
// Useless check if adding a new name/value couple change anything
func (d *DNSQuery) Useless(name, value string, append bool) bool {
// record empty, no issue
if len(d.RRSets) == 0 {
return false
}
alreadySet := false
for _, record := range d.RRSets[0].Records {
if record.Content == value {
alreadySet = true
}
}
return alreadySet && (len(d.RRSets[0].Records) == 1 || append)
}
// RemoveValue remove a value from the first RRSet
func (d *DNSQuery) RemoveValue(value string) bool {
return d.RRSets[0].RemoveValue(value)
}
// EmptyZone marks all lines for deletion
func (d *DNSQuery) EmptyZone() {
for i := range d.RRSets {
d.RRSets[i].Delete()
}
}
func (d DNSQuery) Len() int {
return len(d.RRSets)
}
func (d DNSQuery) Swap(i, j int) {
d.RRSets[i], d.RRSets[j] = d.RRSets[j], d.RRSets[i]
}
func (d DNSQuery) Less(i, j int) bool {
if d.RRSets[i].Type != d.RRSets[j].Type {
if d.RRSets[i].Type == "A" {
return false
}
if d.RRSets[j].Type == "A" {
return true
}
return d.RRSets[i].Type < d.RRSets[j].Type
}
if len(d.RRSets[i].Records) == 0 || len(d.RRSets[j].Records) == 0 {
return d.RRSets[i].Name < d.RRSets[j].Name
}
if d.RRSets[i].Type == d.RRSets[j].Type {
if d.RRSets[i].Records[0].Content == d.RRSets[j].Records[0].Content {
return d.RRSets[i].Name < d.RRSets[j].Name
}
switch d.RRSets[i].Type {
case "PTR":
return ipToInt(ptrToIP(d.RRSets[i].Name)).Cmp(ipToInt(ptrToIP(d.RRSets[j].Name))) < 0
case "CNAME":
return d.RRSets[i].Records[0].Content < d.RRSets[j].Records[0].Content
case "A":
return ipToInt(d.RRSets[i].Records[0].Content).Cmp(ipToInt(d.RRSets[j].Records[0].Content)) < 0
case "AAAA":
return ipToInt(d.RRSets[i].Records[0].Content).Cmp(ipToInt(d.RRSets[j].Records[0].Content)) < 0
}
}
return reverse(d.RRSets[i].Name) < reverse(d.RRSets[j].Name)
}
// RecordSize returns the number of records in the nth RRSets of the query
func (d DNSQuery) RecordSize(n int) int {
if d.Len() < n {
return 0
}
return len(d.RRSets[n].Records)
}
// String() Convert a DNSQuery to a Bind like string
func (d DNSQuery) String() string {
if len(d.RRSets) == 0 {
return ""
}
sort.Sort(d)
result := ""
padName := 40
padTTL := 6
padType := 5
padContent := 0
for _, line := range d.RRSets {
lName := len(line.Name)
if line.IsDeletion() {
lName += 3
}
if lName-len(d.Domain) > padName {
padName = len(line.Name) - len(d.Domain)
}
if len(strconv.Itoa(line.TTL)) > padTTL {
padTTL = len(strconv.Itoa(line.TTL))
}
if len(line.Type) > padType {
padType = len(line.Type)
}
for _, record := range line.Records {
if record.Disabled {
continue
}
if line.Type != "SOA" && len(record.Content) > padContent {
padContent = len(record.Content)
}
}
}
pattern := fmt.Sprintf("%%-%ds %%-%dd IN %%-%ds %%s%%s", padName, padTTL, padType)
for _, line := range d.RRSets {
comment := ""
for _, c := range line.Comment {
if c.Account != "" {
c.Account = " - " + c.Account
}
comment += fmt.Sprintf(" ; %s%s", c.Content, c.Account)
}
sort.Sort(line)
name := line.Name
if line.IsDeletion() {
name = fmt.Sprintf(";; %s", name)
}
idx := strings.LastIndex(name, "."+d.Domain)
if idx > 0 {
name = name[:idx]
}
if name == d.Domain {
name = "@"
}
if len(line.Records) == 0 {
newLine := fmt.Sprintf(pattern, name, line.TTL, line.Type, "", "")
result += strings.Trim(newLine, " ") + "\n"
}
for _, record := range line.Records {
if record.Disabled {
continue
}
newLine := fmt.Sprintf(pattern, name, line.TTL, line.Type, record.Content, comment)
result += strings.Trim(newLine, " ") + "\n"
}
}
if d.Domain != "" {
result = fmt.Sprintf("$ORIGIN %s\n%s", d.Domain, result)
}
return result
}
// AddPlainText set the PlainText of the nth record of the query
func (d *DNSQuery) AddPlainText(n int, s string) {
if d.Len() < n {
return
}
d.RRSets[n].AddPlainText(s)
}
// PlainTexts returns the list of plaintext of each record
func (d *DNSQuery) PlainTexts() []string {
ret := []string{}
for record := range d.RRSets {
if d.RRSets[record].PlainText == "" {
continue
}
ret = append(ret, d.RRSets[record].PlainText)
}
return ret
}
// AddCommentAndTTL adjust the DNSQuery before execution
func (d *DNSQuery) AddCommentAndTTL(user, content string, ttl int) {
// don't touch if the query is not an executable one
if !d.HasDomain() {
return
}
for record := range d.RRSets {
if d.RRSets[record].IsDeletion() {
d.RRSets[record].Comment = nil
continue
}
d.RRSets[record].TTL = ttl
// don't change the comments if you don't have to
if len(content) > 0 {
d.RRSets[record].Comment = []*DNSComments{{
Account: user,
Content: content,
}}
}
}
}
// SplitDeletionQuery split the the result of a GetRecord() for a deletion
// change the DNSQuery RRSets to be valid for the deletion, and return the
// list of affected reverses
func (d *DNSQuery) SplitDeletionQuery(name, value string) ([]*DNSRRSet, bool, error) {
var remain, indirection bool
filter := []*DNSRRSet{}
actions := []*DNSRRSet{}
reverses := []*DNSRRSet{}
for _, entry := range d.RRSets {
switch {
// protect the zone NS
case entry.Type == "NS" && d.Domain+"." == entry.Name:
filter = append(filter, entry)
// protect the SOA
case entry.Type == "SOA":
filter = append(filter, entry)
// simple case : remove every entries
case entry.Name == name && value == "":
entry.Delete()
actions = append(actions, entry)
// a value was specified, need to remove the exact value
case entry.Name == name:
filter = append(filter, entry)
if entry.RemoveValue(value) {
actions = append(actions, entry)
// we keep some entries
remain = remain || entry.Len() > 0
}
// if we remove a wildcard, no need to check the reverse and
// indirections
case name[0] == '*':
continue
case entry.Type != "PTR":
indirection = true
// entry.Type has now to be PTR
case value == "":
entry.RemoveValue(name)
reverses = append(reverses, entry)
case value == ptrToIP(entry.Name):
entry.RemoveValue(name)
reverses = append(reverses, entry)
}
}
// test if there is something to do
if len(actions)+len(filter) == 0 {
return nil, false, fmt.Errorf("Unknown entry")
}
if len(actions) == 0 {
d.RRSets = filter // display only the direct entries, not the indirect ones
return nil, false, nil
}
if indirection && !remain {
return nil, false, fmt.Errorf("there are records pointing to %s, please delete them first", name)
}
d.RRSets = actions
return reverses, true, nil
}
func (d *DNSRRSet) Len() int {
return len(d.Records)
}
func (d *DNSRRSet) Swap(i, j int) {
d.Records[i], d.Records[j] = d.Records[j], d.Records[i]
}
func (d *DNSRRSet) Less(i, j int) bool {
cmp := ipToInt(d.Records[i].Content).Cmp(ipToInt(d.Records[j].Content))
if cmp == 0 {
return d.Records[i].Content < d.Records[j].Content
}
return cmp < 0
}
func (d *DNSRRSet) String() string {
return string(printJSON(d))
}
// RemoveValue remove a value from a RRSet
func (d *DNSRRSet) RemoveValue(value string) bool {
newRecords := []*DNSRecord{}
for _, r := range d.Records {
switch r.Content {
case value:
continue
case addPoint(value):
continue
case fmt.Sprintf("\"%s\"", value):
continue
default:
newRecords = append(newRecords, r)
}
}
// no record removed
if len(newRecords) == d.Len() {
return false
}
// no record left, change the RRSet ChangeType to DELETE
if len(newRecords) == 0 {
d.Delete()
return true
}
d.PlainText = fmt.Sprintf("Update %s by removing %s", d.Name, value)
d.Records = newRecords
return true
}
// DNSQuery return a DNSQuery containing the current DNSRRSet
func (d *DNSRRSet) DNSQuery(domain string) *DNSQuery {
return &DNSQuery{RRSets: []*DNSRRSet{d}, Domain: domain}
}
// AddPlainText set the PlainText of the rrset
func (d *DNSRRSet) AddPlainText(s string) {
d.PlainText = s
}
// Delete change tye RRSet ChangeType to DELETE
func (d *DNSRRSet) Delete() {
d.ChangeType = "DELETE"
d.PlainText = fmt.Sprintf("Removing %s %s", d.Type, d.Name)
}
// IsDeletion tells if d is a DELETE or not
func (d *DNSRRSet) IsDeletion() bool {
return d.ChangeType == "DELETE"
}
func (d DNSRecord) String() string {
return string(printJSON(d))
}
func (d DNSComments) String() string {
return string(printJSON(d))
}