add partially working implementation of /dnsapi endpoint

This commit is contained in:
Philipp Böhm 2017-11-29 00:43:00 +01:00
parent 24b7ea6058
commit b17528ab9a
5 changed files with 47 additions and 215 deletions

View File

@ -1,170 +0,0 @@
package backend
import (
"bufio"
"errors"
"fmt"
"github.com/pboehm/ddns/config"
"github.com/pboehm/ddns/hosts"
"io"
"strings"
"time"
)
// PowerDnsBackend implements the PowerDNS-Pipe-Backend protocol ABI-Version 1
type PowerDnsBackend struct {
config *config.Config
hosts hosts.HostBackend
in io.Reader
out io.Writer
}
// NewPowerDnsBackend creates an instance of a PowerDNS-Pipe-Backend using the supplied parameters
func NewPowerDnsBackend(config *config.Config, backend hosts.HostBackend, in io.Reader, out io.Writer) *PowerDnsBackend {
return &PowerDnsBackend{
config: config,
hosts: backend,
in: in,
out: out,
}
}
// Run reads requests from an input (normally STDIN) and prints response messages to an output (normally STDOUT)
func (b *PowerDnsBackend) Run() {
responses := make(chan backendResponse, 5)
go func() {
for response := range responses {
fmt.Fprintln(b.out, strings.Join(response, "\t"))
}
}()
// handshake with PowerDNS
bio := bufio.NewReader(b.in)
_, _, _ = bio.ReadLine()
responses <- handshakeResponse
for {
request, err := b.parseRequest(bio)
if err != nil {
responses <- failResponse
continue
}
if err = b.handleRequest(request, responses); err != nil {
responses <- newResponse("LOG", err.Error())
}
}
}
// handleRequest handles the supplied request by sending response messages on the supplied responses channel
func (b *PowerDnsBackend) handleRequest(request *backendRequest, responses chan backendResponse) error {
defer b.commitRequest(responses)
responseRecord := request.queryType
var response string
switch request.queryType {
case "SOA":
response = fmt.Sprintf("%s. hostmaster%s. %d 1800 3600 7200 5",
b.config.SOAFqdn, b.config.Domain, b.currentSOASerial())
case "NS":
response = fmt.Sprintf("%s.", b.config.SOAFqdn)
case "A", "AAAA", "ANY":
hostname, err := b.extractHostname(request.queryName)
if err != nil {
return err
}
var host *hosts.Host
if host, err = b.hosts.GetHost(hostname); err != nil {
return err
}
response = host.Ip
responseRecord = "A"
if !host.IsIPv4() {
responseRecord = "AAAA"
}
if (request.queryType == "A" || request.queryType == "AAAA") && request.queryType != responseRecord {
return errors.New("IP address is not valid for requested record")
}
default:
return errors.New("Unsupported query type")
}
responses <- newResponse("DATA", request.queryName, request.queryClass, responseRecord, "10", request.queryId, response)
return nil
}
func (b *PowerDnsBackend) commitRequest(responses chan backendResponse) {
responses <- endResponse
}
// parseRequest reads a line from input and tries to build a request structure from it
func (b *PowerDnsBackend) parseRequest(input *bufio.Reader) (*backendRequest, error) {
line, _, err := input.ReadLine()
if err != nil {
return nil, err
}
parts := strings.Split(string(line), "\t")
if len(parts) != 6 {
return nil, errors.New("Invalid line")
}
return &backendRequest{
query: parts[0],
queryName: parts[1],
queryClass: parts[2],
queryType: parts[3],
queryId: parts[4],
}, nil
}
// extractHostname extract the host part of the fqdn: pi.d.example.org -> pi
func (b *PowerDnsBackend) extractHostname(rawQueryName string) (string, error) {
queryName := strings.ToLower(rawQueryName)
hostname := ""
if strings.HasSuffix(queryName, b.config.Domain) {
hostname = queryName[:len(queryName)-len(b.config.Domain)]
}
if hostname == "" {
return "", errors.New("Query name does not correspond to our domain")
}
return hostname, nil
}
// currentSOASerial get the current SOA serial by returning the current time in seconds
func (b *PowerDnsBackend) currentSOASerial() int64 {
return time.Now().Unix()
}
type backendRequest struct {
query string
queryName string
queryClass string
queryType string
queryId string
}
type backendResponse []string
func newResponse(values ...string) backendResponse {
return values
}
var (
handshakeResponse backendResponse = []string{"OK", "DDNS Backend"}
endResponse backendResponse = []string{"END"}
failResponse backendResponse = []string{"FAIL"}
)

View File

@ -19,10 +19,10 @@ type Request struct {
} }
type Response struct { type Response struct {
QType string QType string `json:"qtype"`
QName string QName string `json:"qname"`
Content string Content string `json:"content"`
TTL int TTL int `json:"ttl"`
} }
type HostLookup struct { type HostLookup struct {
@ -30,6 +30,10 @@ type HostLookup struct {
hosts hosts.HostBackend hosts hosts.HostBackend
} }
func NewHostLookup(config *config.Config, hostsBackend hosts.HostBackend) *HostLookup {
return &HostLookup{config, hostsBackend}
}
func (l *HostLookup) Lookup(request *Request) (*Response, error) { func (l *HostLookup) Lookup(request *Request) (*Response, error) {
responseRecord := request.QType responseRecord := request.QType
responseContent := "" responseContent := ""

40
ddns.go
View File

@ -7,15 +7,9 @@ import (
"github.com/pboehm/ddns/hosts" "github.com/pboehm/ddns/hosts"
"github.com/pboehm/ddns/web" "github.com/pboehm/ddns/web"
"log" "log"
"os"
"strings" "strings"
) )
const (
CmdBackend string = "backend"
CmdWeb string = "web"
)
var serviceConfig *config.Config = &config.Config{} var serviceConfig *config.Config = &config.Config{}
func init() { func init() {
@ -38,11 +32,7 @@ func init() {
"Be more verbose") "Be more verbose")
} }
func usage() { func validateCommandArgs() {
log.Fatal("Usage: ./ddns [backend|web]")
}
func validateCommandArgs(cmd string) {
if serviceConfig.Domain == "" { if serviceConfig.Domain == "" {
log.Fatal("You have to supply the domain via --domain=DOMAIN") log.Fatal("You have to supply the domain via --domain=DOMAIN")
} else if !strings.HasPrefix(serviceConfig.Domain, ".") { } else if !strings.HasPrefix(serviceConfig.Domain, ".") {
@ -50,39 +40,19 @@ func validateCommandArgs(cmd string) {
serviceConfig.Domain = "." + serviceConfig.Domain serviceConfig.Domain = "." + serviceConfig.Domain
} }
if cmd == CmdBackend {
if serviceConfig.SOAFqdn == "" { if serviceConfig.SOAFqdn == "" {
log.Fatal("You have to supply the server FQDN via --soa_fqdn=FQDN") log.Fatal("You have to supply the server FQDN via --soa_fqdn=FQDN")
} }
}
}
func prepareForExecution() string {
flag.Parse()
if len(flag.Args()) != 1 {
usage()
}
cmd := flag.Args()[0]
validateCommandArgs(cmd)
return cmd
} }
func main() { func main() {
cmd := prepareForExecution() flag.Parse()
validateCommandArgs()
redis := hosts.NewRedisBackend(serviceConfig) redis := hosts.NewRedisBackend(serviceConfig)
defer redis.Close() defer redis.Close()
switch cmd { lookup := backend.NewHostLookup(serviceConfig, redis)
case CmdBackend:
backend.NewPowerDnsBackend(serviceConfig, redis, os.Stdin, os.Stdout).Run()
case CmdWeb: web.NewWebService(serviceConfig, redis, lookup).Run()
web.NewWebService(serviceConfig, redis).Run()
default:
usage()
}
} }

View File

@ -1,6 +1,7 @@
package hosts package hosts
import ( import (
"errors"
"github.com/garyburd/redigo/redis" "github.com/garyburd/redigo/redis"
"github.com/pboehm/ddns/config" "github.com/pboehm/ddns/config"
"time" "time"
@ -51,6 +52,10 @@ func (r *RedisBackend) GetHost(name string) (*Host, error) {
return nil, err return nil, err
} }
if len(data) == 0 {
return nil, errors.New("Host does not exist")
}
if err = redis.ScanStruct(data, &host); err != nil { if err = redis.ScanStruct(data, &host); err != nil {
return nil, err return nil, err
} }

View File

@ -2,6 +2,7 @@ package web
import ( import (
"fmt" "fmt"
"github.com/pboehm/ddns/backend"
"github.com/pboehm/ddns/config" "github.com/pboehm/ddns/config"
"github.com/pboehm/ddns/hosts" "github.com/pboehm/ddns/hosts"
"gopkg.in/gin-gonic/gin.v1" "gopkg.in/gin-gonic/gin.v1"
@ -10,17 +11,20 @@ import (
"net" "net"
"net/http" "net/http"
"regexp" "regexp"
"strings"
) )
type WebService struct { type WebService struct {
config *config.Config config *config.Config
hosts hosts.HostBackend hosts hosts.HostBackend
lookup *backend.HostLookup
} }
func NewWebService(config *config.Config, hosts hosts.HostBackend) *WebService { func NewWebService(config *config.Config, hosts hosts.HostBackend, lookup *backend.HostLookup) *WebService {
return &WebService{ return &WebService{
config: config, config: config,
hosts: hosts, hosts: hosts,
lookup: lookup,
} }
} }
@ -37,7 +41,7 @@ func (w *WebService) Run() {
if valid { if valid {
_, err := w.hosts.GetHost(hostname) _, err := w.hosts.GetHost(hostname)
valid = err == nil valid = err != nil
} }
c.JSON(200, gin.H{ c.JSON(200, gin.H{
@ -55,9 +59,9 @@ func (w *WebService) Run() {
var err error var err error
if _, err = w.hosts.GetHost(hostname); err == nil { if h, err := w.hosts.GetHost(hostname); err == nil {
c.JSON(403, gin.H{ c.JSON(403, gin.H{
"error": "This hostname has already been registered.", "error": fmt.Sprintf("This hostname has already been registered. %v", h),
}) })
return return
} }
@ -122,6 +126,25 @@ func (w *WebService) Run() {
}) })
}) })
r.GET("/dnsapi/lookup/:qname/:qtype", func(c *gin.Context) {
request := &backend.Request{
QName: strings.TrimRight(c.Param("qname"), "."),
QType: c.Param("qtype"),
}
response, err := w.lookup.Lookup(request)
if err == nil {
c.JSON(200, gin.H{
"result": []*backend.Response{response},
})
} else {
log.Printf("Error during lookup: %v", err)
c.JSON(200, gin.H{
"result": []string{},
})
}
})
r.Run(w.config.Listen) r.Run(w.config.Listen)
} }