359 lines
7.9 KiB
Go
359 lines
7.9 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 (
|
||
|
dnsApiLight = true
|
||
|
|
||
|
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]
|
||
|
|
||
|
if !dnsApiLight {
|
||
|
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")
|
||
|
}
|
||
|
if len(resp.Answer) == 0 {
|
||
|
log.Printf("got empty answer for %v/%v", qname, dns.TypeToString[qtype])
|
||
|
return []PowerDnsAnswer{}, 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)
|
||
|
}
|
||
|
}
|