468 lines
12 KiB
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))
|
|
}
|