powerdns-remote-http-example/dns-api.go

350 lines
7.7 KiB
Go

package main
// https://docs.powerdns.com/authoritative/backends/remote.html
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/miekg/dns"
)
const (
dnsApiRootUri = "/dns"
dnsApiUriGet = dnsApiRootUri + "/:method/*parameters"
dnsApiUriPostForm = dnsApiRootUri + "/:method"
dnsApiUriPostJson = dnsApiRootUri
)
var (
dnsApiMethods = map[string]dnsApiHandlerFunc{
"lookup": dnsApi_lookup,
"getAllDomains": dnsApi_emptyArray,
"getDomainMetadata": dnsApi_emptyArray,
"getAllDomainMetadata": dnsApi_emptyMap,
}
)
func setupDnsApi(r *gin.Engine) {
r.GET(dnsApiUriGet, dnsApiHttpGetHandler)
// https://docs.powerdns.com/authoritative/backends/remote.html#http-connector
// "post" in "remote-connection-string="
r.POST(dnsApiUriPostForm, dnsApiHttpPostFormHandler)
// https://docs.powerdns.com/authoritative/backends/remote.html#http-connector
// "post,post_json" in "remote-connection-string="
r.POST(dnsApiUriPostJson, dnsApiHttpPostJsonHandler)
}
func dnsApiNotImplemented(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"result": false})
}
func dnsApiSimpleError(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"result": false})
}
func dnsApiGenericError(c *gin.Context, err error) {
log.Printf("error: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"result": false, "error": err.Error()})
}
func dnsApiMissingParameters(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"result": false, "error": "missing parameters"})
}
func dnsApiArgumentError(c *gin.Context, argument string, err error) {
log.Printf("argument %v error: %v", argument, err)
c.JSON(http.StatusBadRequest, gin.H{"result": false, "error": err.Error()})
}
func dnsApiMethodError(c *gin.Context, method string, err error) {
log.Printf("method %v returned error: %v", method, err)
c.JSON(http.StatusBadRequest, gin.H{"result": false, "error": err.Error()})
}
type PowerDnsJsonRequest struct {
Method string `json:"method"`
Parameters map[string]interface{} `json:"parameters"`
}
func NewPowerDnsRequest() PowerDnsJsonRequest {
var r PowerDnsJsonRequest
r.Parameters = make(map[string]interface{})
return r
}
type dnsApiHandlerFunc func(*PowerDnsJsonRequest) (interface{}, error)
func dnsApiHttpGetHandler(c *gin.Context) {
var err error
method := c.Param("method")
if method == "" {
dnsApiSimpleError(c)
return
}
mFunc, mFound := dnsApiMethods[method]
if !mFound {
dnsApiNotImplemented(c)
return
}
// HTTP GET scheme is somewhat spoiled
uriParams := make([]string, 0)
uriParamPrefix := dnsApiRootUri + "/" + method + "/"
uriParamString, hasUriParams := strings.CutPrefix(c.Request.RequestURI, uriParamPrefix)
if hasUriParams {
if uriParamString == "" {
dnsApiMissingParameters(c)
return
}
uriParams = strings.Split(uriParamString, "/")
} else {
uriParamString = ""
}
req := NewPowerDnsRequest()
req.Method = method
// HTTP GET scheme is somewhat spoiled, part 2
switch method {
case "lookup":
switch len(uriParams) {
case 0:
dnsApiArgumentError(c, "qname", err)
return
case 1:
dnsApiArgumentError(c, "qtype", err)
return
}
req.Parameters["qname"] = uriParams[0]
req.Parameters["qtype"] = uriParams[1]
for _, p := range []string{"remote", "local", "real-remote"} {
val := c.Request.Header.Get("X-RemoteBackend-" + p)
if val == "" {
continue
}
req.Parameters[p] = val
}
for _, p := range []string{"zone-id", "zone_id"} {
val := c.Request.Header.Get("X-RemoteBackend-" + p)
if val == "" {
continue
}
req.Parameters["zone-id"], err = strconv.ParseInt(val, 10, 64)
if err != nil {
dnsApiArgumentError(c, "zone-id", err)
return
}
}
case "getAllDomains":
req.Parameters["include_disabled"], err = strconv.ParseBool(c.DefaultQuery("include_disabled", "false"))
if err != nil {
dnsApiArgumentError(c, "include_disabled", err)
return
}
case "getAllDomainMetadata":
if len(uriParams) == 0 {
dnsApiArgumentError(c, "name", err)
return
}
req.Parameters["name"] = uriParams[0]
case "getDomainMetadata":
switch len(uriParams) {
case 0:
dnsApiArgumentError(c, "name", err)
return
case 1:
dnsApiArgumentError(c, "kind", err)
return
}
req.Parameters["name"] = uriParams[0]
req.Parameters["kind"] = uriParams[1]
}
result, err := mFunc(&req)
if err != nil {
dnsApiMethodError(c, method, err)
return
}
c.JSON(http.StatusOK, gin.H{"result": result})
}
func dnsApiHttpPostFormHandler(c *gin.Context) {
var err error
method := c.Param("method")
if method == "" {
dnsApiSimpleError(c)
return
}
mFunc, mFound := dnsApiMethods[method]
if !mFound {
dnsApiNotImplemented(c)
return
}
params := c.PostForm("parameters")
if params == "" {
dnsApiMissingParameters(c)
return
}
if strings.HasPrefix(params, "%7b") {
params, err = url.PathUnescape(params)
if err != nil {
dnsApiGenericError(c, err)
return
}
}
req := NewPowerDnsRequest()
req.Method = method
if strings.HasPrefix(params, "{") {
err = json.Unmarshal([]byte(params), &req.Parameters)
if err != nil {
dnsApiGenericError(c, err)
return
}
}
result, err := mFunc(&req)
if err != nil {
dnsApiMethodError(c, method, err)
return
}
c.JSON(http.StatusOK, gin.H{"result": result})
}
func dnsApiHttpPostJsonHandler(c *gin.Context) {
var err error
bodyData, err := io.ReadAll(c.Request.Body)
if err != nil {
dnsApiGenericError(c, err)
return
}
req := NewPowerDnsRequest()
err = json.Unmarshal(bodyData, &req)
if err != nil {
dnsApiGenericError(c, err)
return
}
method := req.Method
if method == "" {
dnsApiSimpleError(c)
return
}
mFunc, mFound := dnsApiMethods[method]
if !mFound {
dnsApiNotImplemented(c)
return
}
result, err := mFunc(&req)
if err != nil {
dnsApiMethodError(c, method, err)
return
}
c.JSON(http.StatusOK, gin.H{"result": result})
}
func dnsApi_emptyArray(r *PowerDnsJsonRequest) (interface{}, error) {
emptyArray := make([]int, 0)
return emptyArray, nil
}
func dnsApi_emptyMap(r *PowerDnsJsonRequest) (interface{}, error) {
emptyMap := make(map[string]interface{}, 0)
return emptyMap, nil
}
func dnsApi_lookup(r *PowerDnsJsonRequest) (interface{}, error) {
x, ok := r.Parameters["qtype"]
if !ok {
return nil, errors.New("qtype is absent")
}
qtype, ok := x.(string)
if !ok {
return nil, errors.New("qtype is not a string")
}
if qtype == "" {
return nil, errors.New("qtype is empty")
}
dns_qtype := dnsQtypeStringToValue(qtype)
if dns_qtype == dns.TypeNone {
return nil, errors.New("qtype is NONE")
}
x, ok = r.Parameters["qname"]
if !ok {
return nil, errors.New("qname is absent")
}
qname, ok := x.(string)
if !ok {
return nil, errors.New("qname is not a string")
}
if qname == "" {
return nil, errors.New("qname is empty")
}
// adjust qname
if !strings.HasSuffix(qname, ".") {
qname = qname + "."
}
return dnsApi_lookup_int(qname, dns_qtype)
}
func dnsApi_lookup_int(qname string, qtype uint16) (interface{}, error) {
if qtype == dns.TypeSOA {
return []PowerDnsAnswer{
{
Qname: qname,
Qtype: dns.TypeToString[qtype],
Ttl: cfgTtlMax,
Content: fmt.Sprintf("%v %v %v %v %v %v %v",
// ns mbox serial
cfgSoaNs, cfgSoaMbox, time.Now().Unix(),
// refresh retry expire minttl
cfgTtlMax/2, cfgTtlMax, cfgTtlMax*2, cfgTtlMin,
),
},
}, nil
}
resp, err := dnsCustomResolve(qname, qtype)
if err != nil {
return nil, err
}
if resp == nil {
return nil, errors.New("dns.Msg is nil")
}
switch qtype {
case dns.TypeA, dns.TypeAAAA, dns.TypeANY:
// resolve and process (see below)
return dnsRemap(qname, qtype, resp)
default:
// only resolve
return dnsToPowerDnsAnswer(resp)
}
}