Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
David Prandzioch committed Dec 30, 2019
2 parents d05a661 + 9a5f524 commit 284dee5
Show file tree
Hide file tree
Showing 8 changed files with 403 additions and 242 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
language: go
go:
- 1.7.x
- 1.8.5

services:
- docker
Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,56 @@ http://myhost.mydomain.tld:8080/update?secret=changeme&domain=foo&addr=1.2.3.4
redirected to the same domain separated by comma, so "foo,bar"
* `addr`: IPv4 or IPv6 address of the name record


For the DynDNS compatible fields please see Dyn's documentation here:

```
https://help.dyn.com/remote-access-api/perform-update/
```


### DynDNS compatible API

This package contains a DynDNS compatible handler for convenience and for use cases
where clients cannot be modified to use the JSON responses and/or URL scheme outlined
above.

This has been tested with a number of routers. Just point the router to your DDNS domain
for updates.

The handlers will listen on:
* /nic/update
* /v2/update
* /v3/update


**The username is not validated at all so you can use anything as a username**
**Password is the shared secret provided as an ENV variable**

#### Examples

An example on the ddclient (Linux DDNS client) based Ubiquiti router line:

set service dns dynamic interface eth0 service dyndns host-name <your-ddns-hostname-to-be-updated>
set service dns dynamic interface eth0 service dyndns login <anything-as-username-is-not-validated>
set service dns dynamic interface eth0 service dyndns password <shared-secret>
set service dns dynamic interface eth0 service dyndns protocol dyndns2
set service dns dynamic interface eth0 service dyndns server <your-ddns-server>

Optional if you used this behind an HTTPS reverse proxy like I do:

set service dns dynamic interface eth0 service dyndns options ssl=true

This also means that DDCLIENT works out of the box and Linux based devices should work.

D-Link DIR-842:

Another router that has been tested is from the D-Link router line where you need to fill the
details in on the Web Interface. The values are self-explanatory. Under the server (once you chosen Manual)
you need to enter you DDNS server's hostname or IP. The protocol used by the router will be the
dyndns2 by default and cannot be changed.


## Accessing the REST API log

Just run
Expand Down
10 changes: 5 additions & 5 deletions rest-api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import (
)

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

func (conf *Config) LoadConfig(path string) {
Expand Down
23 changes: 11 additions & 12 deletions rest-api/ipparser/ipparser.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
package ipparser

import (
"net"
"net"
)

func ValidIP4(ipAddress string) bool {
testInput := net.ParseIP(ipAddress)
if testInput == nil {
return false
}
testInput := net.ParseIP(ipAddress)
if testInput == nil {
return false
}

return (testInput.To4() != nil)
return (testInput.To4() != nil)
}

func ValidIP6(ip6Address string) bool {
testInputIP6 := net.ParseIP(ip6Address)
if testInputIP6 == nil {
return false
}
testInputIP6 := net.ParseIP(ip6Address)
if testInputIP6 == nil {
return false
}

return (testInputIP6.To16() != nil)
return (testInputIP6.To16() != nil)
}

28 changes: 14 additions & 14 deletions rest-api/ipparser_test.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
package main

import (
"testing"
"dyndns/ipparser"
"dyndns/ipparser"
"testing"
)

func TestValidIP4ToReturnTrueOnValidAddress(t *testing.T) {
result := ipparser.ValidIP4("1.2.3.4")
result := ipparser.ValidIP4("1.2.3.4")

if result != true {
t.Fatalf("Expected ValidIP(1.2.3.4) to be true but got false")
}
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")
result := ipparser.ValidIP4("abcd")

if result == true {
t.Fatalf("Expected ValidIP(abcd) to be false but got true")
}
if result == true {
t.Fatalf("Expected ValidIP(abcd) to be false but got true")
}
}

func TestValidIP4ToReturnFalseOnEmptyAddress(t *testing.T) {
result := ipparser.ValidIP4("")
result := ipparser.ValidIP4("")

if result == true {
t.Fatalf("Expected ValidIP() to be false but got true")
}
if result == true {
t.Fatalf("Expected ValidIP() to be false but got true")
}
}
190 changes: 121 additions & 69 deletions rest-api/main.go
Original file line number Diff line number Diff line change
@@ -1,86 +1,138 @@
package main

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

"github.com/gorilla/mux"
"bufio"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"

"github.com/gorilla/mux"
)

var appConfig = &Config{}

func main() {
appConfig.LoadConfig("/etc/dyndns.json")
appConfig.LoadConfig("/etc/dyndns.json")

router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/update", Update).Methods("GET")
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/update", Update).Methods("GET")

log.Println(fmt.Sprintf("Serving dyndns REST services on 0.0.0.0:8080..."))
log.Fatal(http.ListenAndServe(":8080", router))
}

func Update(w http.ResponseWriter, r *http.Request) {
response := BuildWebserviceResponseFromRequest(r, appConfig)

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

for _, domain := range response.Domains {
result := UpdateRecord(domain, response.Address, response.AddrType)
/* 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")

if result != "" {
response.Success = false
response.Message = result

json.NewEncoder(w).Encode(response)
return
}
}
log.Println(fmt.Sprintf("Serving dyndns REST services on 0.0.0.0:8080..."))
log.Fatal(http.ListenAndServe(":8080", router))
}

response.Success = true
response.Message = fmt.Sprintf("Updated %s record for %s to IP address %s", response.AddrType, response.Domain, response.Address)
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")
}

return sharedSecret
},
Domain: func(r *http.Request) string { return r.URL.Query().Get("hostname") },
}
response := BuildWebserviceResponseFromRequest(r, appConfig, extractor)

if response.Success == false {
if response.Message == "Domain not set" {
w.Write([]byte("notfqdn\n"))
} else {
w.Write([]byte("badauth\n"))
}
return
}

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

if result != "" {
response.Success = false
response.Message = result

w.Write([]byte("dnserr\n"))
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)))
}

json.NewEncoder(w).Encode(response)
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)

if response.Success == false {
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

json.NewEncoder(w).Encode(response)
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))

f, err := ioutil.TempFile(os.TempDir(), "dyndns")
if err != nil {
return err.Error()
}

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 out.String()
log.Println(fmt.Sprintf("%s record update request: %s -> %s", addrType, domain, ipaddr))

f, err := ioutil.TempFile(os.TempDir(), "dyndns")
if err != nil {
return err.Error()
}

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 out.String()
}
Loading

0 comments on commit 284dee5

Please sign in to comment.