add partially working implementation of /dnsapi endpoint
This commit is contained in:
parent
24b7ea6058
commit
b17528ab9a
|
@ -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"}
|
|
||||||
)
|
|
|
@ -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
40
ddns.go
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
31
web/web.go
31
web/web.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue