diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d1da3a5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +[1.1.0] +* Update Debian Jessie to Debian Stretch +* Multistage Dockerfile resulting in smaller production image +* Code refactoring +* Extended response +* Basic unit test coverage +* Documentation on running from DockerHub + +[1.0.0] +* Initial release diff --git a/Dockerfile b/Dockerfile index 7569c8e..9b9ea54 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,25 @@ -FROM debian:jessie -MAINTAINER David Prandzioch - +FROM debian:stretch as builder RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ - apt-get install -q -y bind9 dnsutils golang git-core && \ + apt-get install -q -y golang git-core && \ apt-get clean -RUN chmod 770 /var/cache/bind - -COPY setup.sh /root/setup.sh -RUN chmod +x /root/setup.sh - ENV GOPATH=/root/go RUN mkdir -p /root/go/src COPY rest-api /root/go/src/dyndns -RUN cd /root/go/src/dyndns && go get +RUN cd /root/go/src/dyndns && go get && go test -v + +FROM debian:stretch +MAINTAINER David Prandzioch +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + apt-get install -q -y bind9 dnsutils && \ + 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 EXPOSE 53 8080 -CMD ["sh", "-c", "/root/setup.sh ; service bind9 start ; /root/go/bin/dyndns"] +CMD ["sh", "-c", "/root/setup.sh ; service bind9 start ; /root/dyndns"] diff --git a/Makefile b/Makefile index 9d2e79d..d3af69e 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,28 @@ image: - docker build -t davd/dyndns-server . + docker build -t davd/docker-ddns:latest . console: - docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --rm davd/dyndns-server bash + 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 server_test: - docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --rm davd/dyndns-server + 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" api_test: - curl "http://localhost:8080/update?secret=changeme&domain=foo&addr=1.2.3.4" - dig @localhost foo.example.org + curl "http://docker.local:8080/update?secret=changeme&domain=foo&addr=1.2.3.4" + dig @docker.local foo.example.org + +api_test_invalid_params: + curl "http://docker.local:8080/update?secret=changeme&addr=1.2.3.4" + dig @docker.local foo.example.org api_test_recursion: - dig @localhost google.com + dig @docker.local google.com deploy: image - docker run -it -d -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --name=dyndns davd/dyndns-server \ No newline at end of file + docker run -it -d -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --name=dyndns davd/docker-ddns:latest diff --git a/rest-api/ipparser/ipparser.go b/rest-api/ipparser/ipparser.go new file mode 100644 index 0000000..44545d2 --- /dev/null +++ b/rest-api/ipparser/ipparser.go @@ -0,0 +1,24 @@ +package ipparser + +import ( + "net" +) + +func ValidIP4(ipAddress string) bool { + testInput := net.ParseIP(ipAddress) + if testInput == nil { + return false + } + + return (testInput.To4() != nil) +} + +func ValidIP6(ip6Address string) bool { + testInputIP6 := net.ParseIP(ip6Address) + if testInputIP6 == nil { + return false + } + + return (testInputIP6.To16() != nil) +} + diff --git a/rest-api/ipparser_test.go b/rest-api/ipparser_test.go new file mode 100644 index 0000000..7fbfdf1 --- /dev/null +++ b/rest-api/ipparser_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "testing" + "dyndns/ipparser" +) + +func TestValidIP4ToReturnTrueOnValidAddress(t *testing.T) { + result := ipparser.ValidIP4("1.2.3.4") + + if result != true { + t.Fatalf("Expected ValidIP(1.2.3.4) to be true but got false") + } +} + +func TestValidIP4ToReturnFalseOnInvalidAddress(t *testing.T) { + result := ipparser.ValidIP4("abcd") + + if result == true { + t.Fatalf("Expected ValidIP(abcd) to be false but got true") + } +} + +func TestValidIP4ToReturnFalseOnEmptyAddress(t *testing.T) { + result := ipparser.ValidIP4("") + + if result == true { + t.Fatalf("Expected ValidIP() to be false but got true") + } +} diff --git a/rest-api/main.go b/rest-api/main.go index 44f483d..a09b3f4 100644 --- a/rest-api/main.go +++ b/rest-api/main.go @@ -10,18 +10,12 @@ import ( "os/exec" "bytes" "encoding/json" - "net" "github.com/gorilla/mux" ) var appConfig = &Config{} -type WebserviceResponse struct { - Success bool - Message string -} - func main() { appConfig.LoadConfig("/etc/dyndns.json") @@ -32,70 +26,15 @@ func main() { log.Fatal(http.ListenAndServe(":8080", router)) } -func validIP4(ipAddress string) bool { - testInput := net.ParseIP(ipAddress) - if testInput == nil { - return false - } - - return (testInput.To4() != nil) -} - -func validIP6(ip6Address string) bool { - testInputIP6 := net.ParseIP(ip6Address) - if testInputIP6 == nil { - return false - } - - return (testInputIP6.To16() != nil) -} - func Update(w http.ResponseWriter, r *http.Request) { - response := WebserviceResponse{} - - var sharedSecret string - var domain string - var address string - - vals := r.URL.Query() - sharedSecret = vals["secret"][0] - domain = vals["domain"][0] - address = vals["addr"][0] - - if sharedSecret != appConfig.SharedSecret { - log.Println(fmt.Sprintf("Invalid shared secret: %s", sharedSecret)) - response.Success = false - response.Message = "Invalid Credentials" - json.NewEncoder(w).Encode(response) - return; - } - - w.Header().Set("Content-Type", "application/json") - - var addrType string - - if validIP4(address) { - addrType = "A" - } else if validIP6(address) { - addrType = "AAAA" - } else { - response.Success = false - response.Message = fmt.Sprintf("%s is neither a valid IPv4 nor IPv6 address", address) - } - - if addrType != "" { - if domain == "" { - response.Success = false - response.Message = fmt.Sprintf("Domain not set", address) - log.Println(fmt.Sprintf("Domain not set")) - return; - } + response := BuildWebserviceResponseFromRequest(r, appConfig) - result := UpdateRecord(domain, address, addrType) + if response.Success { + result := UpdateRecord(response.Domain, response.Address, response.AddrType) if result == "" { response.Success = true - response.Message = fmt.Sprintf("Updated %s record for %s to IP address %s", addrType, domain, address) + response.Message = fmt.Sprintf("Updated %s record for %s to IP address %s", response.AddrType, response.Domain, response.Address) } else { response.Success = false response.Message = result diff --git a/rest-api/request_handler.go b/rest-api/request_handler.go new file mode 100644 index 0000000..7181614 --- /dev/null +++ b/rest-api/request_handler.go @@ -0,0 +1,57 @@ +package main + +import ( + "log" + "fmt" + "net/http" + + "dyndns/ipparser" +) + +type WebserviceResponse struct { + Success bool + Message string + Domain string + Address string + AddrType string +} + +func BuildWebserviceResponseFromRequest(r *http.Request, appConfig *Config) WebserviceResponse { + response := WebserviceResponse{} + + var sharedSecret string + + vals := r.URL.Query() + sharedSecret = vals.Get("secret") + response.Domain = vals.Get("domain") + response.Address = vals.Get("addr") + + if sharedSecret != appConfig.SharedSecret { + log.Println(fmt.Sprintf("Invalid shared secret: %s", sharedSecret)) + response.Success = false + response.Message = "Invalid Credentials" + return response + } + + if response.Domain == "" { + response.Success = false + response.Message = fmt.Sprintf("Domain not set") + log.Println("Domain not set") + return response + } + + if ipparser.ValidIP4(response.Address) { + response.AddrType = "A" + } else if ipparser.ValidIP6(response.Address) { + response.AddrType = "AAAA" + } else { + response.Success = false + response.Message = fmt.Sprintf("%s is neither a valid IPv4 nor IPv6 address", response.Address) + log.Println(fmt.Sprintf("Invalid address: %s", response.Address)) + return response + } + + response.Success = true + + return response +} diff --git a/rest-api/request_handler_test.go b/rest-api/request_handler_test.go new file mode 100644 index 0000000..6e3b797 --- /dev/null +++ b/rest-api/request_handler_test.go @@ -0,0 +1,90 @@ +package main + +import ( + "testing" + "net/http" +) + +func TestBuildWebserviceResponseFromRequestToReturnValidObject(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("POST", "/update?secret=changeme&domain=foo&addr=1.2.3.4", nil) + result := BuildWebserviceResponseFromRequest(req, appConfig) + + if result.Success != true { + t.Fatalf("Expected WebserviceResponse.Success to be true") + } + + if result.Domain != "foo" { + t.Fatalf("Expected WebserviceResponse.Domain to be foo") + } + + if result.Address != "1.2.3.4" { + t.Fatalf("Expected WebserviceResponse.Address to be 1.2.3.4") + } + + if result.AddrType != "A" { + t.Fatalf("Expected WebserviceResponse.AddrType to be A") + } +} + +func TestBuildWebserviceResponseFromRequestToReturnInvalidObjectWhenNoSecretIsGiven(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("POST", "/update", nil) + result := BuildWebserviceResponseFromRequest(req, appConfig) + + if result.Success != false { + t.Fatalf("Expected WebserviceResponse.Success to be false") + } +} + +func TestBuildWebserviceResponseFromRequestToReturnInvalidObjectWhenInvalidSecretIsGiven(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("POST", "/update?secret=foo", nil) + result := BuildWebserviceResponseFromRequest(req, appConfig) + + if result.Success != false { + t.Fatalf("Expected WebserviceResponse.Success to be false") + } +} + +func TestBuildWebserviceResponseFromRequestToReturnInvalidObjectWhenNoDomainIsGiven(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("POST", "/update?secret=changeme", nil) + result := BuildWebserviceResponseFromRequest(req, appConfig) + + if result.Success != false { + t.Fatalf("Expected WebserviceResponse.Success to be false") + } +} + +func TestBuildWebserviceResponseFromRequestToReturnInvalidObjectWhenNoAddressIsGiven(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("POST", "/update?secret=changeme&domain=foo", nil) + result := BuildWebserviceResponseFromRequest(req, appConfig) + + if result.Success != false { + t.Fatalf("Expected WebserviceResponse.Success to be false") + } +} + +func TestBuildWebserviceResponseFromRequestToReturnInvalidObjectWhenInvalidAddressIsGiven(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("POST", "/update?secret=changeme&domain=foo&addr=1.41:2", nil) + result := BuildWebserviceResponseFromRequest(req, appConfig) + + if result.Success != false { + t.Fatalf("Expected WebserviceResponse.Success to be false") + } +}