pdns-auth-proxy/jsonrpc.go

700 lines
19 KiB
Go

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
}