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) } }