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)) }