diff --git a/backend/backend.go b/backend/backend.go deleted file mode 100644 index 94f0772..0000000 --- a/backend/backend.go +++ /dev/null @@ -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"} -) diff --git a/backend/lookup.go b/backend/lookup.go index 51d8c20..10c5903 100644 --- a/backend/lookup.go +++ b/backend/lookup.go @@ -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 := "" diff --git a/ddns.go b/ddns.go index 089b05b..10f0abc 100644 --- a/ddns.go +++ b/ddns.go @@ -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() } diff --git a/hosts/redis.go b/hosts/redis.go index 1d9dc0c..89d91ab 100644 --- a/hosts/redis.go +++ b/hosts/redis.go @@ -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 } diff --git a/web/web.go b/web/web.go index 8a35e07..d5e8bb0 100644 --- a/web/web.go +++ b/web/web.go @@ -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) }