package main import ( "encoding/json" "errors" "fmt" "log" "net" "regexp" "strings" ) type ( // JSONArray is an array of JSONInput JSONArray []*JSONInput // JSONRPCResponse is a jsonRPC response structure JSONRPCResponse struct { ID int `json:"id"` JSONRPC string `json:"jsonrpc"` Result []*JSONRPCResult `json:"result"` } // JSONRPCError is a jsonRPC error structure JSONRPCError struct { ID int `json:"id"` JSONRPC string `json:"jsonrpc"` Error struct { Code int `json:"code"` Message string `json:"message"` } `json:"error"` } // JSONInput is a json rpc 2.0 compatible structure for the PDNS API JSONInput struct { Method string `json:"method"` Params JSONInputParams `json:"params"` ID int `json:"id"` ignoreBadDomain bool user string listFilters []*regexp.Regexp } // JSONInputParams encode the parameters of the method JSONInputParams struct { Name string `json:"name"` Value string `json:"value"` TTL int `json:"ttl"` ForceReverse bool `json:"reverse"` Append bool `json:"append"` Priority int `json:"priority"` Comment string `json:"comment"` DryRun bool `json:"dry-run"` IgnoreError bool `json:"ignore-error"` Nonce string `json:"nonce"` } // JSONRPCResult is the type of the response of the API JSONRPCResult struct { Changes string `json:"changes"` Comment string `json:"comment"` Result string `json:"result"` Raw []interface{} `json:"raw"` Error string `json:"error"` } ) // JSONRPCResult creates new result func (j JSONInput) JSONRPCResult(content, comment string, err error) *JSONRPCResult { ret := &JSONRPCResult{ Changes: content, Comment: comment, Raw: []interface{}{}, } if err != nil { ret.Comment = "There was an error" ret.Error = err.Error() } return ret } // IsError returns if this response is an error or not func (r *JSONRPCResult) IsError() bool { return len(r.Error) > 0 } // JSONInput try to convert back a JSONInput to the original command line func (j JSONInput) String() string { ret := "" if j.Params.TTL != 172800 || j.Method == "ttl" { ret += fmt.Sprintf("-t %d ", j.Params.TTL) } if j.Params.ForceReverse { ret += "-f " } if j.Params.Append { ret += "-a " } if j.Method == "mx" { ret += fmt.Sprintf("-p %d ", j.Params.Priority) } if len(j.Params.Comment) > 0 { ret += fmt.Sprintf("-c \"%s\" ", j.Params.Comment) } if j.Params.DryRun { ret += "-n " } return fmt.Sprintf("%s %s %s %s", ret, j.Method, j.Params.Name, j.Params.Value) } // SetDefaults modify the input by setting the right default parameters func (ja JSONArray) SetDefaults(p *PowerDNS) JSONArray { for i, j := range ja { if j.Params.TTL == 0 { ja[i].Params.TTL = p.DefaultTTL } } return ja } // Run the actions defined in the json structure func (ja JSONArray) Run(h *HTTPServer, user string, wasArray, textOnly bool) string { listResult := []interface{}{} plain := "" for _, j := range ja { result := j.Run(h, user) for _, line := range result { plain = plain + line.Changes } log.Printf("[jsonRPC API] User %s used command \"jsonrpc %s\"\n", user, j.String()) if len(result) == 0 { listResult = append(listResult, JSONRPCResponse{ID: j.ID, JSONRPC: "2.0"}) continue } // check if the last entry of the result is an error last := result[len(result)-1] if !last.IsError() { listResult = append(listResult, JSONRPCResponse{ID: j.ID, JSONRPC: "2.0", Result: result}) continue } listResult = append(listResult, JSONRPCNewError(-32000, j.ID, last.Error)) h.dns.LogDebug(last.Error) if !j.Params.IgnoreError { break } } if textOnly { return plain } if wasArray { return string(printJSON(listResult)) } if len(listResult) > 0 { return string(printJSON(listResult[0])) } return "" } // Run the action defined in the json structure func (j *JSONInput) Run(h *HTTPServer, user string) []*JSONRPCResult { // store the username j.user = user ret := []*JSONRPCResult{} // normalize the query, and do some checks if err := j.Normalize(); err != nil { return append(ret, j.JSONRPCResult("", "", err)) } switch j.Method { // list is a spacial case, it doesn't imply a DNSQuery() object case "list": result, err := h.dns.ListZones(j.Params.Name) // we apply the acl after the fact for the list method result = j.FilterList(result) if err == nil && len(result) == 0 { err = errors.New("Unknown domain") } res := j.JSONRPCResult(result.List("\n"), "", err) for i := range result { res.Raw = append(res.Raw, result[i].Name) } return append(ret, res) case "domain": parentName, err := h.dns.GetDomain(j.Params.Name) res := j.JSONRPCResult(parentName, "", err) res.Raw = append(res.Raw, parentName) return append(ret, res) } actions, err := j.DNSQueries(h) if err != nil { return append(ret, j.JSONRPCResult("", "", err)) } for _, act := range actions { // always add a comment if j.Params.Comment == "" && !j.Params.DryRun { j.Params.Comment = "-" } // add comment, ttl, and cleanup the payload if j.Method != "dump" && j.Method != "search" { act.AddCommentAndTTL(user, j.Params.Comment, j.Params.TTL) } result := j.JSONRPCResult("", strings.Join(act.PlainTexts(), "\n"), nil) result.Changes = act.String() result.Raw = append(result.Raw, act) // if we are in dry run mode, stop here if j.Params.DryRun { result.Result = "Dry Run, nothing done" ret = append(ret, result) continue } // no domain, it will failed if executed // can be the output of a zone creation if !act.HasDomain() { ret = append(ret, result) continue } code, _, err := h.dns.Execute(act) switch { case err == nil && code == 204: result.Result = "Command Successfull" case err != nil: result.Error = err.Error() default: result.Error = fmt.Sprintf("The return code was %d", code) } ret = append(ret, result) if err != nil { break } } return ret } // Normalize make some case change on the JSONInput and limit the wildcards in // the name func (j *JSONInput) Normalize() error { // normalize Name (lower case, add a final .) j.Params.Name = strings.ToLower(j.Params.Name) j.Params.Name = addPoint(j.Params.Name) if j.Method != "txt" { j.Params.Value = strings.ToLower(j.Params.Value) } switch j.Method { case "list": case "search": case "newzone": if !validName(j.Params.Name, false) { return errors.New("invalid name") } case "ptr": if !validName(j.Params.Name, false) { return errors.New("invalid name") } case "srv": if !validSRVName(j.Params.Name) { return errors.New("invalid name") } case "txt": j.Params.Value = addQuotes(j.Params.Value) case "domain": if !validName(j.Params.Name, false) { return errors.New("invalid name") } default: if !validName(j.Params.Name, true) { return errors.New("invalid name") } } // add a final . to the value for _, m := range []string{"cname", "mx", "dname", "ns", "ptr", "srv"} { if j.Method == m { j.Params.Value = addPoint(j.Params.Value) } } return nil } // DNSQueries takes a JSONInput and returns a usable []DNSQuery to be sent to // pdns. It can change the content of j to force dry run mode func (j *JSONInput) DNSQueries(h *HTTPServer) ([]*DNSQuery, error) { var err error switch j.Method { case "search": result, err := h.dns.Search(j.Params.Name) // we apply the acl after the fact for the search method result = j.FilterSearch(result) j.Params.DryRun = true return []*DNSQuery{result.DNSQuery()}, err case "dump": result, err := h.dns.Zone(j.Params.Name) j.Params.DryRun = true return []*DNSQuery{result}, err case "newzone": newZone, otherActions, err := j.NewZone(h) if err != nil { return nil, err } if j.Params.DryRun { return append(otherActions, newZone.TransformIntoDNSQuery()), nil } result := &DNSQuery{} code, _, err := h.dns.ExecuteZone(newZone, result) if err == nil && code != 201 { err = fmt.Errorf("The return code was %d for the creation of zone %s", code, j.Params.Name) } return append(otherActions, result), err } current, err := h.dns.GetRecord(j.Params.Name, j.Method, j.ignoreBadDomain) if err != nil { return nil, err } switch j.Method { case "ttl": return j.DNSQueriesTTL(current) case "delete": return j.DNSQueriesDelete(h, current) } // test if there is something to add if current.Useless(j.Params.Name, j.Params.Value, j.Params.Append) { j.Params.DryRun = true current.AddPlainText(0, "Nothing to do, the record is unchanged") return []*DNSQuery{current}, nil } switch j.Method { case "ns": return j.DNSQueriesNS(h, current) case "a": return j.DNSQueriesA(h, current) case "aaaa": return j.DNSQueriesA(h, current) case "cname": if err = j.CheckCNAME(h); err == nil { return j.DNSQueriesGeneric(current) } case "dname": if err = j.CheckCNAME(h); err == nil { return j.DNSQueriesGeneric(current) } case "mx": if err = j.CheckMX(h); err == nil { return j.DNSQueriesGeneric(current) } case "srv": if err = j.CheckSRV(h); err == nil { return j.DNSQueriesGeneric(current) } case "ptr": if err = j.CheckPTR(h); err == nil { return j.DNSQueriesGeneric(current) } case "caa": if err = j.CheckCAA(); err == nil { return j.DNSQueriesGeneric(current) } case "txt": return j.DNSQueriesGeneric(current) default: err = errors.New("unknown action") } return nil, err } // DNSQueriesGeneric is the DNSQueries method for the most commands func (j *JSONInput) DNSQueriesGeneric(current *DNSQuery) ([]*DNSQuery, error) { // just change the value current.ChangeValue(j.Params.Name, j.Params.Value, j.Method, current.Len() > 0 && !j.Params.Append, false) return []*DNSQuery{current}, nil } // DNSQueriesNS change the NS of a record func (j *JSONInput) DNSQueriesNS(h *HTTPServer, current *DNSQuery) ([]*DNSQuery, error) { if trimPoint(j.Params.Name) == trimPoint(current.Domain) { return nil, fmt.Errorf("You cannot change the NS of a local zone") } subZone, err := h.dns.Zone(j.Params.Name) if err != nil { return nil, err } currentNS := map[string]bool{j.Params.Value: true} currentGlue := map[string]bool{} for _, entry := range subZone.RRSets { if entry.Type == "NS" && entry.Name == j.Params.Name { for _, v := range entry.Records { currentNS[v.Content] = true } continue } if entry.Type == "A" || entry.Type == "AAAA" { currentGlue[entry.Name] = true continue } return nil, fmt.Errorf( "There are records that will be masked if you delegate the zone\nPlease delete them first") } fmt.Println(currentNS, currentGlue) for ns := range currentNS { if !strings.HasSuffix(ns, "."+addPoint(j.Params.Name)) { continue } if _, ok := currentGlue[ns]; !ok { return nil, fmt.Errorf("You must first create a glue record to resolve %s", j.Params.Value) } } for glue := range currentGlue { if _, ok := currentNS[glue]; !ok { return nil, fmt.Errorf( "There are records that will be masked if you delegate the zone\nPlease delete them first") } } current.ChangeValue(j.Params.Name, j.Params.Value, j.Method, !j.Params.Append, false) return []*DNSQuery{current}, nil } // DNSQueriesTTL change the TTL of a record func (j *JSONInput) DNSQueriesTTL(current *DNSQuery) ([]*DNSQuery, error) { todo := []int{} for i := range current.RRSets { if current.RRSets[i].TTL == j.Params.TTL { continue } if j.Params.Value == "" { todo = append(todo, i) continue } for _, v := range current.RRSets[i].Records { if v.Content == j.Params.Value { todo = append(todo, i) break } } } if len(todo) == 0 { j.Params.DryRun = true return nil, fmt.Errorf("Nothing to do, the record is unchanged") } newRRSets := []*DNSRRSet{} for _, i := range todo { newRRSets = append(newRRSets, current.RRSets[i]) } current.RRSets = newRRSets return []*DNSQuery{current}, nil } // DNSQueriesA is the DNSQueries method for the A and AAAA commands // it checks the command is legal, and can make reverse DNS changes if needed func (j *JSONInput) DNSQueriesA(h *HTTPServer, forward *DNSQuery) ([]*DNSQuery, error) { reverse, err := h.dns.GetReverse(j.Params.Value, j.Method == "a") if err != nil { return nil, err } // If we manage the reverse .arpa zone, we set it if either there is no // reverse, or the forceReverse parameter is set askForReverse := reverse != nil && (j.Params.ForceReverse || reverse.Len() == 0) if askForReverse && j.Params.Name[0] == '*' { return nil, errors.New("Can't set a reverse to a wildcard") } // simple case : the name didn't exist, or we do an append if forward.Len() == 0 || j.Params.Append { forward.ChangeValue(j.Params.Name, j.Params.Value, j.Method, false, askForReverse) return []*DNSQuery{forward}, nil } actions, err := h.dns.ReverseChanges(forward, j.Params.Value) forward.ChangeValue(j.Params.Name, j.Params.Value, j.Method, true, askForReverse) return append(actions, forward), err } // DNSQueriesDelete is the DNSQueries method for the Delete command func (j *JSONInput) DNSQueriesDelete(h *HTTPServer, current *DNSQuery) ([]*DNSQuery, error) { reverses, useful, err := current.SplitDeletionQuery(j.Params.Name, j.Params.Value) if err != nil { return nil, err } if !useful { j.Params.DryRun = true current.AddPlainText(0, "Nothing to do, the record is unchanged") return []*DNSQuery{current}, nil } ret := []*DNSQuery{current} // add the reverse changes if needed for _, r := range reverses { parentName, err := h.dns.GetDomain(r.Name) if err != nil { return nil, err } ret = append(ret, r.DNSQuery(parentName)) if !r.IsDeletion() { continue } ip := ptrToIP(r.Name) if h.dns.IsUsed(ip, j.Params.Name, []string{"A", "AAAA"}) { message := "Reverse issue : %s is the reverse for %s and will be removed\n" message += "But other records are pointing to %s as well. Please cleanup first\n" return ret, fmt.Errorf(message, j.Params.Name, ip, ip) } } return ret, nil } // CheckPTR validates that the query is a valid PTR func (j *JSONInput) CheckPTR(h *HTTPServer) error { ip := ptrToIP(j.Params.Name) if ip == "" { return fmt.Errorf("%s is not a valid PTR", j.Params.Name) } target := &DNSQuery{} if err := h.dns.CanCreate(j.Params.Value, true, target); err != nil || target.Len() == 0 { return err } for _, rec := range target.RRSets[0].Records { if rec.Content == ip { return nil } } return fmt.Errorf("%s must point to %s", j.Params.Value, ip) } // CheckSRV validates that the query is a valid SRV func (j *JSONInput) CheckSRV(h *HTTPServer) error { name := validSRV(j.Params.Value) if name == "" { return fmt.Errorf("%s is not a valid SRV", j.Params.Value) } return h.dns.CanCreate(name, false, nil) } // CheckCAA validates that the query is a valid CAA func (j *JSONInput) CheckCAA() error { v := validCAA(j.Params.Value) if v == "" { return fmt.Errorf("%s is not a valid CAA", j.Params.Value) } j.Params.Value = v return nil } // CheckMX validates that the query is a valid MX func (j *JSONInput) CheckMX(h *HTTPServer) error { name := validMX(j.Params.Value) if name == "" { return fmt.Errorf("%s is not a valid MX", j.Params.Value) } return h.dns.CanCreate(name, true, nil) } // CheckCNAME validates that the query is a valid CNAME func (j *JSONInput) CheckCNAME(h *HTTPServer) error { test := net.ParseIP(trimPoint(j.Params.Value)) if test != nil { return fmt.Errorf("%s is an IP, not a DNS Name", j.Params.Value) } if !validName(j.Params.Value, false) { return fmt.Errorf("%s is not a valid DNS Name", j.Params.Value) } return h.dns.CanCreate(j.Params.Value, false, nil) } // NewZone create a new zone in the DNS. // If the new zone is a subzone of an existing one, it will move potential // existing entries into the new zone func (j *JSONInput) NewZone(h *HTTPServer) (z *DNSZone, otherActions []*DNSQuery, err error) { // get the zone parameters zoneType, soa, nameServers, defaults, autoInc, err := h.GetZoneConfig(j.Params.Name) if err != nil { return nil, nil, err } if soa == "" { soa = fmt.Sprintf("%s hostmaster.%s 0 28800 7200 604800 86400", nameServers[0], j.Params.Name) } parentName, zoneErr := h.dns.GetDomain(j.Params.Name) parentName = trimPoint(parentName) z, err = h.dns.NewZone(j.Params.Name, zoneType, soa, j.user, j.Params.Comment, j.Params.TTL, nameServers, autoInc) if err != nil { return nil, nil, err } // check if there is a parent zone if zoneErr == nil && parentName == trimPoint(j.Params.Name) { return nil, nil, fmt.Errorf("%s already exists", j.Params.Name) } // we didn't find a parent zone, we must create the default entries if zoneErr != nil { for _, d := range defaults { // make a copy entry := *d if d.Params.Name == "" { entry.Params.Name = j.Params.Name } else { entry.Params.Name = fmt.Sprintf("%s.%s", d.Params.Name, j.Params.Name) } if err := entry.Normalize(); err != nil { h.dns.LogDebug(err) continue } actions, err := entry.DNSQueries(h) if err != nil { return nil, nil, err } for _, a := range actions { a.ChangeDomain(j.Params.Name) a.AddCommentAndTTL(j.user, j.Params.Comment, j.Params.TTL) z.AddEntries(a) } } return } // we must create the NS records in the parent zone too glue, err := h.dns.SetNameServers(j.Params.Name, parentName, nameServers) if err != nil { return nil, nil, err } otherActions = append(otherActions, glue) // check if there are records in the parent zone records, err := h.dns.Zone(j.Params.Name) if err != nil { err = nil return } // if there are records, we must add them in the new zone... records.SetPlainTexts("Add", false) z.AddEntries(records) // and delete them in the parent delete, err := h.dns.Zone(j.Params.Name) if err != nil { return nil, nil, err } delete.EmptyZone() delete.ChangeDomain(parentName) otherActions = append(otherActions, delete) return } // FilterSearch prune the DNSSearch according to the restrictions in j.listFilters func (j *JSONInput) FilterSearch(z []*DNSSearchEntry) []*DNSSearchEntry { if len(z) == 0 { return z } good := []int{} for i := range z { name := trimPoint(z[i].Name) for _, re := range j.listFilters { if re.MatchString(name) { good = append(good, i) break } } } newList := []*DNSSearchEntry{} for _, i := range good { newList = append(newList, z[i]) } return newList } // FilterList prune the DNSZones according to the restrictions in j.listFilters func (j *JSONInput) FilterList(z []*DNSZone) []*DNSZone { if len(z) == 0 { return z } good := []int{} for i := range z { name := trimPoint(z[i].Name) for _, re := range j.listFilters { if re.MatchString(name) { good = append(good, i) break } } } newList := []*DNSZone{} for _, i := range good { newList = append(newList, z[i]) } return newList } // ParsejsonRPCRequest read the payload of the query and put it in the structure func ParsejsonRPCRequest(s []byte, d *PowerDNS) (JSONArray, bool, error) { var inSimple *JSONInput var inArray JSONArray if json.Unmarshal(s, &inArray); len(inArray) > 0 { return inArray.SetDefaults(d), true, nil } if err := json.Unmarshal(s, &inSimple); err != nil { return nil, false, err } return JSONArray{inSimple}.SetDefaults(d), false, nil } func isDryRun(j JSONArray) bool { for _, cmd := range j { if !cmd.Params.DryRun { return false } } return true }