Skip to content

Make the go program easier to use as a standalone program: make it a go module, add command line flags and support for ddnskey #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
FROM debian:buster as builder
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -q -y golang git-core && \
apt-get clean
FROM golang:1.14 as builder

ENV GOPATH=/root/go
RUN mkdir -p /root/go/src
Expand All @@ -12,14 +9,14 @@ FROM debian:buster-slim
MAINTAINER David Prandzioch <[email protected]>

RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -q -y bind9 dnsutils && \
apt-get install -q -y bind9 dnsutils openssl && \
apt-get clean

RUN chmod 770 /var/cache/bind
COPY setup.sh /root/setup.sh
RUN chmod +x /root/setup.sh
COPY named.conf.options /etc/bind/named.conf.options
COPY --from=builder /root/go/bin/dyndns /root/dyndns
COPY --from=builder /root/go/bin/rest-api /root/dyndns

EXPOSE 53 8080
CMD ["sh", "-c", "/root/setup.sh ; service bind9 start ; /root/dyndns"]
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ console:
docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --rm davd/docker-ddns:latest bash

devconsole:
docker run -it --rm -v ${PWD}/rest-api:/usr/src/app -w /usr/src/app golang:1.8.5 bash
docker run -it --rm -v ${PWD}/rest-api:/usr/src/app -w /usr/src/app golang:1.14 bash

server_test:
docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --rm davd/docker-ddns:latest

unit_tests:
docker run -it --rm -v ${PWD}/rest-api:/go/src/dyndns -w /go/src/dyndns golang:1.8.5 /bin/bash -c "go get && go test -v"
docker run -it --rm -v ${PWD}/rest-api:/go/src/dyndns -w /go/src/dyndns golang:1.14 /bin/bash -c "go get && go test -v"

api_test:
curl "http://localhost:8080/update?secret=changeme&domain=foo&addr=1.2.3.4"
Expand Down
36 changes: 33 additions & 3 deletions rest-api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,26 @@ package main

import (
"encoding/json"
"flag"
"os"
)

type Config struct {
SharedSecret string
Server string
Zone string
Domain string
NsupdateBinary string
RecordTTL int
Port int
}

func (conf *Config) LoadConfig(path string) {
type ConfigFlags struct {
Config
ConfigFile string
DoNotLoadConfig bool
LogLevel int
}

func (conf *Config) loadConfigFromFile(path string) {
file, err := os.Open(path)
if err != nil {
panic(err)
Expand All @@ -25,3 +32,26 @@ func (conf *Config) LoadConfig(path string) {
panic(err)
}
}

func (flagsConf *ConfigFlags) setupFlags() {
flag.BoolVar(&flagsConf.DoNotLoadConfig, "noConfig", false, "Do not load the config file")
flag.StringVar(&flagsConf.ConfigFile, "c", "/etc/dyndns.json", "The configuration file")
flag.StringVar(&flagsConf.Server, "server", "localhost", "The address of the bind server")
flag.StringVar(&flagsConf.Zone, "zone", "", "Configuring a default zone will allow to send request with the hostname only as the domain")
flag.StringVar(&flagsConf.NsupdateBinary, "nsupdateBinary", "nsupdate", "Path to nsupdate program")
flag.IntVar(&flagsConf.RecordTTL, "recordTTL", 300, "RecordTTL")
flag.IntVar(&flagsConf.Port, "p", 8080, "Port")
flag.IntVar(&flagsConf.LogLevel, "log", 0, "Set the log level")
}

// LoadConfig loads config values from the config file and from the passed arguments.
// Gives command line arguments precedence.
func (flagsConf *ConfigFlags) LoadConfig() {
flagsConf.setupFlags()
flag.Parse()

if !flagsConf.DoNotLoadConfig {
flagsConf.loadConfigFromFile(flagsConf.ConfigFile)
flag.Parse() // Parse a second time to overwrite settings from the loaded file
}
}
5 changes: 5 additions & 0 deletions rest-api/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/dprandzioch/docker-ddns/rest-api

go 1.14

require github.com/gorilla/mux v1.8.0
2 changes: 2 additions & 0 deletions rest-api/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
2 changes: 1 addition & 1 deletion rest-api/ipparser_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package main

import (
"dyndns/ipparser"
"github.com/dprandzioch/docker-ddns/rest-api/ipparser"
"testing"
)

Expand Down
174 changes: 88 additions & 86 deletions rest-api/main.go
Original file line number Diff line number Diff line change
@@ -1,52 +1,48 @@
package main

import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"

"github.com/gorilla/mux"
)

var appConfig = &Config{}
type key int

const (
responseKey key = iota
extractorKey key = iota
)

var appConfig = &ConfigFlags{}

func main() {
appConfig.LoadConfig("/etc/dyndns.json")
defaultExtractor := defaultRequestDataExtractor{appConfig: &appConfig.Config}
dynExtractor := dynRequestDataExtractor{defaultRequestDataExtractor{appConfig: &appConfig.Config}}

appConfig.LoadConfig()

router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/update", Update).Methods("GET")
router.Handle("/update", requestRequestDataMiddleware(http.HandlerFunc(update), defaultExtractor)).Methods(http.MethodGet)
router.Handle("/delete", requestRequestDataMiddleware(http.HandlerFunc(update), defaultExtractor)).Methods(http.MethodGet, http.MethodDelete)

/* DynDNS compatible handlers. Most routers will invoke /nic/update */
router.HandleFunc("/nic/update", DynUpdate).Methods("GET")
router.HandleFunc("/v2/update", DynUpdate).Methods("GET")
router.HandleFunc("/v3/update", DynUpdate).Methods("GET")
router.Handle("/nic/update", requestRequestDataMiddleware(http.HandlerFunc(dynUpdate), dynExtractor)).Methods(http.MethodGet)
router.Handle("/v2/update", requestRequestDataMiddleware(http.HandlerFunc(dynUpdate), dynExtractor)).Methods(http.MethodGet)
router.Handle("/v3/update", requestRequestDataMiddleware(http.HandlerFunc(dynUpdate), dynExtractor)).Methods(http.MethodGet)

log.Println(fmt.Sprintf("Serving dyndns REST services on 0.0.0.0:8080..."))
log.Fatal(http.ListenAndServe(":8080", router))
}
listenTo := fmt.Sprintf("%s:%d", "", appConfig.Port)

func DynUpdate(w http.ResponseWriter, r *http.Request) {
extractor := RequestDataExtractor{
Address: func(r *http.Request) string { return r.URL.Query().Get("myip") },
Secret: func(r *http.Request) string {
_, sharedSecret, ok := r.BasicAuth()
if !ok || sharedSecret == "" {
sharedSecret = r.URL.Query().Get("password")
}
log.Println(fmt.Sprintf("Serving dyndns REST services on " + listenTo + "..."))
log.Fatal(http.ListenAndServe(listenTo, router))
}

return sharedSecret
},
Domain: func(r *http.Request) string { return r.URL.Query().Get("hostname") },
}
response := BuildWebserviceResponseFromRequest(r, appConfig, extractor)
func dynUpdate(w http.ResponseWriter, r *http.Request) {
response := r.Context().Value(responseKey).(WebserviceResponse)

if response.Success == false {
if !response.Success {
if response.Message == "Domain not set" {
w.Write([]byte("notfqdn\n"))
} else {
Expand All @@ -55,84 +51,90 @@ func DynUpdate(w http.ResponseWriter, r *http.Request) {
return
}

for _, domain := range response.Domains {
result := UpdateRecord(domain, response.Address, response.AddrType)

if result != "" {
response.Success = false
response.Message = result
success := updateDomains(r, &response, func() {
w.Write([]byte("dnserr\n"))
})

w.Write([]byte("dnserr\n"))
return
}
if !success {
return
}

response.Success = true
response.Message = fmt.Sprintf("Updated %s record for %s to IP address %s", response.AddrType, response.Domain, response.Address)

w.Write([]byte(fmt.Sprintf("good %s\n", response.Address)))
}

func Update(w http.ResponseWriter, r *http.Request) {
extractor := RequestDataExtractor{
Address: func(r *http.Request) string { return r.URL.Query().Get("addr") },
Secret: func(r *http.Request) string { return r.URL.Query().Get("secret") },
Domain: func(r *http.Request) string { return r.URL.Query().Get("domain") },
}
response := BuildWebserviceResponseFromRequest(r, appConfig, extractor)
func update(w http.ResponseWriter, r *http.Request) {
response := r.Context().Value(responseKey).(WebserviceResponse)

if response.Success == false {
if !response.Success {
json.NewEncoder(w).Encode(response)
return
}

for _, domain := range response.Domains {
result := UpdateRecord(domain, response.Address, response.AddrType)

if result != "" {
response.Success = false
response.Message = result
success := updateDomains(r, &response, func() {
json.NewEncoder(w).Encode(response)
})

json.NewEncoder(w).Encode(response)
return
}
if !success {
return
}

response.Success = true
response.Message = fmt.Sprintf("Updated %s record for %s to IP address %s", response.AddrType, response.Domain, response.Address)

json.NewEncoder(w).Encode(response)
}

func UpdateRecord(domain string, ipaddr string, addrType string) string {
log.Println(fmt.Sprintf("%s record update request: %s -> %s", addrType, domain, ipaddr))
func updateDomains(r *http.Request, response *WebserviceResponse, onError func()) bool {
extractor := r.Context().Value(extractorKey).(requestDataExtractor)

for _, record := range response.Records {
for _, domain := range response.Domains {
recordUpdate := RecordUpdateRequest{
domain: domain,
ipAddr: record.Value,
addrType: record.Type,
secret: extractor.Secret(r),
ddnsKeyName: extractor.DdnsKeyName(r, domain),
zone: extractor.Zone(r, domain),
fqdn: extractor.Fqdn(r, domain),
action: extractor.Action(r),
}
result, err := recordUpdate.updateRecord()

if err != nil {
response.Success = false
response.Message = err.Error()

f, err := ioutil.TempFile(os.TempDir(), "dyndns")
if err != nil {
return err.Error()
onError()
return false
}
response.Success = true
if len(response.Message) != 0 {
response.Message += "; "
}
response.Message += result
}
}

defer os.Remove(f.Name())
w := bufio.NewWriter(f)

w.WriteString(fmt.Sprintf("server %s\n", appConfig.Server))
w.WriteString(fmt.Sprintf("zone %s\n", appConfig.Zone))
w.WriteString(fmt.Sprintf("update delete %s.%s %s\n", domain, appConfig.Domain, addrType))
w.WriteString(fmt.Sprintf("update add %s.%s %v %s %s\n", domain, appConfig.Domain, appConfig.RecordTTL, addrType, ipaddr))
w.WriteString("send\n")

w.Flush()
f.Close()

cmd := exec.Command(appConfig.NsupdateBinary, f.Name())
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
err = cmd.Run()
if err != nil {
return err.Error() + ": " + stderr.String()
return true
}

func (r RecordUpdateRequest) updateRecord() (string, error) {
var nsupdate NSUpdateInterface = NewNSUpdate()
message := "No action executed"
switch r.action {
case UpdateRequestActionDelete:
nsupdate.DeleteRecord(r)
message = fmt.Sprintf("Deleted %s record for %s", r.addrType, r.domain)
case UpdateRequestActionUpdate:
fallthrough
default:
nsupdate.UpdateRecord(r)
message = fmt.Sprintf("Updated %s record: %s -> %s", r.addrType, r.domain, r.ipAddr)
}
result := nsupdate.Close()

log.Println(message)

return out.String()
if result != "" {
return "", fmt.Errorf("%s", result)
}
return message, nil
}
Loading