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

44
ddns.go
View File

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

View File

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

View File

@ -2,6 +2,7 @@ package web
import (
"fmt"
"github.com/pboehm/ddns/backend"
"github.com/pboehm/ddns/config"
"github.com/pboehm/ddns/hosts"
"gopkg.in/gin-gonic/gin.v1"
@ -10,17 +11,20 @@ import (
"net"
"net/http"
"regexp"
"strings"
)
type WebService struct {
config *config.Config
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{
config: config,
hosts: hosts,
lookup: lookup,
}
}
@ -37,7 +41,7 @@ func (w *WebService) Run() {
if valid {
_, err := w.hosts.GetHost(hostname)
valid = err == nil
valid = err != nil
}
c.JSON(200, gin.H{
@ -55,9 +59,9 @@ func (w *WebService) Run() {
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{
"error": "This hostname has already been registered.",
"error": fmt.Sprintf("This hostname has already been registered. %v", h),
})
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)
}