From ef96496474397e16e1cc4c34e4a3724f795e6e3c Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 27 Jul 2021 13:12:16 +0200 Subject: [PATCH] Added more documentation and prepared some structural changes --- dyndns/handler/handler.go | 12 +- dyndns/handler/host.go | 62 +++++++---- dyndns/handler/log.go | 12 +- dyndns/ipparser/ipparser.go | 6 +- dyndns/model/host.go | 3 + dyndns/model/log.go | 1 + dyndns/{handler/update.go => nswrapper/ip.go} | 104 +++--------------- dyndns/nswrapper/update.go | 85 ++++++++++++++ 8 files changed, 169 insertions(+), 116 deletions(-) rename dyndns/{handler/update.go => nswrapper/ip.go} (52%) create mode 100644 dyndns/nswrapper/update.go diff --git a/dyndns/handler/handler.go b/dyndns/handler/handler.go index dc390c4..6d0842c 100644 --- a/dyndns/handler/handler.go +++ b/dyndns/handler/handler.go @@ -2,13 +2,14 @@ package handler import ( "fmt" + "os" + "strings" + "github.com/benjaminbear/docker-ddns-server/dyndns/model" "github.com/go-playground/validator/v10" "github.com/jinzhu/gorm" "github.com/labstack/echo/v4" "github.com/tg123/go-htpasswd" - "os" - "strings" ) type Handler struct { @@ -27,6 +28,7 @@ type CustomValidator struct { Validator *validator.Validate } +// Validate implements the Validator. func (cv *CustomValidator) Validate(i interface{}) error { return cv.Validator.Struct(i) } @@ -35,6 +37,8 @@ type Error struct { Message string `json:"message"` } +// Authenticate is the method the website admin user and the host update user have to authenticate against. +// To gather admin rights the username password combination must match with the credentials given by the env var. func (h *Handler) Authenticate(username, password string, c echo.Context) (bool, error) { h.AuthHost = nil h.AuthAdmin = false @@ -76,6 +80,9 @@ func (h *Handler) authByEnv(username, password string) (bool, error) { return false, nil } +// ParseEnvs parses all needed environment variables: +// DDNS_ADMIN_LOGIN: The basic auth login string in htpasswd style. +// DDNS_DOMAINS: All domains that will be handled by the dyndns server. func (h *Handler) ParseEnvs() error { h.Config = Envs{} h.Config.AdminLogin = os.Getenv("DDNS_ADMIN_LOGIN") @@ -91,6 +98,7 @@ func (h *Handler) ParseEnvs() error { return nil } +// InitDB creates an empty database and creates all tables if there isn't already one, or opens the existing one. func (h *Handler) InitDB() (err error) { if _, err := os.Stat("database"); os.IsNotExist(err) { err = os.MkdirAll("database", os.ModePerm) diff --git a/dyndns/handler/host.go b/dyndns/handler/host.go index 0b02465..f0ef076 100644 --- a/dyndns/handler/host.go +++ b/dyndns/handler/host.go @@ -2,18 +2,26 @@ package handler import ( "fmt" - "github.com/benjaminbear/docker-ddns-server/dyndns/model" - "github.com/jinzhu/gorm" - "github.com/labstack/echo/v4" "net" "net/http" "strconv" "time" + + "github.com/benjaminbear/docker-ddns-server/dyndns/nswrapper" + + "github.com/benjaminbear/docker-ddns-server/dyndns/model" + "github.com/jinzhu/gorm" + "github.com/labstack/echo/v4" ) +const ( + UNAUTHORIZED = "You are not allowed to view that content" +) + +// GetHost fetches a host from the database by "id". func (h *Handler) GetHost(c echo.Context) (err error) { if !h.AuthAdmin { - return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"}) + return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED}) } id, err := strconv.Atoi(c.Param("id")) @@ -30,9 +38,10 @@ func (h *Handler) GetHost(c echo.Context) (err error) { return c.JSON(http.StatusOK, id) } +// ListHosts fetches all hosts from database and lists them on the website. func (h *Handler) ListHosts(c echo.Context) (err error) { if !h.AuthAdmin { - return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"}) + return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED}) } hosts := new([]model.Host) @@ -45,9 +54,10 @@ func (h *Handler) ListHosts(c echo.Context) (err error) { }) } +// AddHost just renders the "add host" website. func (h *Handler) AddHost(c echo.Context) (err error) { if !h.AuthAdmin { - return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"}) + return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED}) } return c.Render(http.StatusOK, "edithost", echo.Map{ @@ -56,9 +66,10 @@ func (h *Handler) AddHost(c echo.Context) (err error) { }) } +// EditHost fetches a host by "id" and renders the "edit host" website. func (h *Handler) EditHost(c echo.Context) (err error) { if !h.AuthAdmin { - return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"}) + return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED}) } id, err := strconv.Atoi(c.Param("id")) @@ -78,9 +89,12 @@ func (h *Handler) EditHost(c echo.Context) (err error) { }) } +// CreateHost validates the host data from the "add host" website, +// adds the host entry to the database, +// and adds the entry to the DNS server. func (h *Handler) CreateHost(c echo.Context) (err error) { if !h.AuthAdmin { - return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"}) + return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED}) } host := &model.Host{} @@ -98,12 +112,12 @@ func (h *Handler) CreateHost(c echo.Context) (err error) { // If a ip is set create dns entry if host.Ip != "" { - ipType := getIPType(host.Ip) + ipType := nswrapper.GetIPType(host.Ip) if ipType == "" { return c.JSON(http.StatusBadRequest, &Error{fmt.Sprintf("ip %s is not a valid ip", host.Ip)}) } - if err = h.updateRecord(host.Hostname, host.Ip, ipType, host.Domain, host.Ttl); err != nil { + if err = nswrapper.UpdateRecord(host.Hostname, host.Ip, ipType, host.Domain, host.Ttl); err != nil { return c.JSON(http.StatusBadRequest, &Error{err.Error()}) } } @@ -111,9 +125,12 @@ func (h *Handler) CreateHost(c echo.Context) (err error) { return c.JSON(http.StatusOK, host) } +// UpdateHost validates the host data from the "edit host" website, +// and compares the host data with the entry in the database by "id". +// If anything has changed the database and DNS entries for the host will be updated. func (h *Handler) UpdateHost(c echo.Context) (err error) { if !h.AuthAdmin { - return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"}) + return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED}) } hostUpdate := &model.Host{} @@ -142,12 +159,12 @@ func (h *Handler) UpdateHost(c echo.Context) (err error) { // If ip or ttl changed update dns entry if forceRecordUpdate { - ipType := getIPType(host.Ip) + ipType := nswrapper.GetIPType(host.Ip) if ipType == "" { return c.JSON(http.StatusBadRequest, &Error{fmt.Sprintf("ip %s is not a valid ip", host.Ip)}) } - if err = h.updateRecord(host.Hostname, host.Ip, ipType, host.Domain, host.Ttl); err != nil { + if err = nswrapper.UpdateRecord(host.Hostname, host.Ip, ipType, host.Domain, host.Ttl); err != nil { return c.JSON(http.StatusBadRequest, &Error{err.Error()}) } } @@ -155,9 +172,11 @@ func (h *Handler) UpdateHost(c echo.Context) (err error) { return c.JSON(http.StatusOK, host) } +// DeleteHost fetches a host entry from the database by "id" +// and deletes the database and DNS server entry to it. func (h *Handler) DeleteHost(c echo.Context) (err error) { if !h.AuthAdmin { - return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"}) + return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED}) } id, err := strconv.Atoi(c.Param("id")) @@ -185,23 +204,26 @@ func (h *Handler) DeleteHost(c echo.Context) (err error) { return c.JSON(http.StatusBadRequest, &Error{err.Error()}) } - if err = h.deleteRecord(host.Hostname, host.Domain); err != nil { + if err = nswrapper.DeleteRecord(host.Hostname, host.Domain); err != nil { return c.JSON(http.StatusBadRequest, &Error{err.Error()}) } return c.JSON(http.StatusOK, id) } +// UpdateIP implements the update method called by the routers. +// Hostname, IP and senders IP are validated, a log entry is created +// and finally if everything is ok, the DNS Server will be updated func (h *Handler) UpdateIP(c echo.Context) (err error) { if h.AuthHost == nil { return c.String(http.StatusBadRequest, "badauth\n") } - log := &model.Log{Status: false, Host: *h.AuthHost, TimeStamp: time.Now(), UserAgent: shrinkUserAgent(c.Request().UserAgent())} + log := &model.Log{Status: false, Host: *h.AuthHost, TimeStamp: time.Now(), UserAgent: nswrapper.ShrinkUserAgent(c.Request().UserAgent())} log.SentIP = c.QueryParam(("myip")) // Get caller IP - log.CallerIP, err = getCallerIP(c.Request()) + log.CallerIP, err = nswrapper.GetCallerIP(c.Request()) if log.CallerIP == "" { log.CallerIP, _, err = net.SplitHostPort(c.Request().RemoteAddr) if err != nil { @@ -226,10 +248,10 @@ func (h *Handler) UpdateIP(c echo.Context) (err error) { } // Get IP type - ipType := getIPType(log.SentIP) + ipType := nswrapper.GetIPType(log.SentIP) if ipType == "" { log.SentIP = log.CallerIP - ipType = getIPType(log.SentIP) + ipType = nswrapper.GetIPType(log.SentIP) if ipType == "" { log.Message = "Bad Request: Sent IP is invalid" if err = h.CreateLogEntry(log); err != nil { @@ -241,7 +263,7 @@ func (h *Handler) UpdateIP(c echo.Context) (err error) { } // add/update DNS record - if err = h.updateRecord(log.Host.Hostname, log.SentIP, ipType, log.Host.Domain, log.Host.Ttl); err != nil { + if err = nswrapper.UpdateRecord(log.Host.Hostname, log.SentIP, ipType, log.Host.Domain, log.Host.Ttl); err != nil { log.Message = fmt.Sprintf("DNS error: %v", err) if err = h.CreateLogEntry(log); err != nil { fmt.Println(err) diff --git a/dyndns/handler/log.go b/dyndns/handler/log.go index 2e3240f..d7ced67 100644 --- a/dyndns/handler/log.go +++ b/dyndns/handler/log.go @@ -1,12 +1,14 @@ package handler import ( - "github.com/benjaminbear/docker-ddns-server/dyndns/model" - "github.com/labstack/echo/v4" "net/http" "strconv" + + "github.com/benjaminbear/docker-ddns-server/dyndns/model" + "github.com/labstack/echo/v4" ) +// CreateLogEntry simply adds a log entry to the database. func (h *Handler) CreateLogEntry(log *model.Log) (err error) { if err = h.DB.Create(log).Error; err != nil { return err @@ -15,9 +17,10 @@ func (h *Handler) CreateLogEntry(log *model.Log) (err error) { return nil } +// ShowLogs fetches all log entries from all hosts and renders them to the website. func (h *Handler) ShowLogs(c echo.Context) (err error) { if !h.AuthAdmin { - return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"}) + return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED}) } logs := new([]model.Log) @@ -30,9 +33,10 @@ func (h *Handler) ShowLogs(c echo.Context) (err error) { }) } +// ShowHostLogs fetches all log entries of a specific host by "id" and renders them to the website. func (h *Handler) ShowHostLogs(c echo.Context) (err error) { if !h.AuthAdmin { - return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"}) + return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED}) } id, err := strconv.Atoi(c.Param("id")) diff --git a/dyndns/ipparser/ipparser.go b/dyndns/ipparser/ipparser.go index b93977f..4e39a0c 100644 --- a/dyndns/ipparser/ipparser.go +++ b/dyndns/ipparser/ipparser.go @@ -4,20 +4,22 @@ import ( "net" ) +// ValidIP4 tells you if a given string is a valid IPv4 address. func ValidIP4(ipAddress string) bool { testInput := net.ParseIP(ipAddress) if testInput == nil { return false } - return (testInput.To4() != nil) + return testInput.To4() != nil } +// ValidIP6 tells you if a given string is a valid IPv6 address. func ValidIP6(ip6Address string) bool { testInputIP6 := net.ParseIP(ip6Address) if testInputIP6 == nil { return false } - return (testInputIP6.To16() != nil) + return testInputIP6.To16() != nil } diff --git a/dyndns/model/host.go b/dyndns/model/host.go index 3145edb..af3e084 100644 --- a/dyndns/model/host.go +++ b/dyndns/model/host.go @@ -6,6 +6,7 @@ import ( "github.com/jinzhu/gorm" ) +// Host is a dns host entry. type Host struct { gorm.Model Hostname string `gorm:"unique_index:idx_host_domain;not null" form:"hostname" validate:"required,hostname"` @@ -17,6 +18,8 @@ type Host struct { Password string `form:"password" validate:"min=8"` } +// UpdateHost updates all fields of a host entry +// and sets a new LastUpdate date. func (h *Host) UpdateHost(updateHost *Host) (updateRecord bool) { updateRecord = false if h.Ip != updateHost.Ip || h.Ttl != updateHost.Ttl { diff --git a/dyndns/model/log.go b/dyndns/model/log.go index 50764aa..057b25b 100644 --- a/dyndns/model/log.go +++ b/dyndns/model/log.go @@ -6,6 +6,7 @@ import ( "github.com/jinzhu/gorm" ) +// Log defines a log entry. type Log struct { gorm.Model Status bool diff --git a/dyndns/handler/update.go b/dyndns/nswrapper/ip.go similarity index 52% rename from dyndns/handler/update.go rename to dyndns/nswrapper/ip.go index 7ccb064..4f18195 100644 --- a/dyndns/handler/update.go +++ b/dyndns/nswrapper/ip.go @@ -1,93 +1,18 @@ -package handler +package nswrapper import ( - "bufio" "bytes" "errors" "fmt" - "github.com/benjaminbear/docker-ddns-server/dyndns/ipparser" - "io/ioutil" "net" "net/http" - "os" - "os/exec" "strings" + + "github.com/benjaminbear/docker-ddns-server/dyndns/ipparser" ) -func (h *Handler) updateRecord(hostname string, ipAddr string, addrType string, zone string, ttl int) error { - fmt.Printf("%s record update request: %s -> %s\n", addrType, hostname, ipAddr) - - f, err := ioutil.TempFile(os.TempDir(), "dyndns") - if err != nil { - return err - } - - defer os.Remove(f.Name()) - w := bufio.NewWriter(f) - - w.WriteString(fmt.Sprintf("server %s\n", "localhost")) - w.WriteString(fmt.Sprintf("zone %s\n", zone)) - w.WriteString(fmt.Sprintf("update delete %s.%s %s\n", hostname, zone, addrType)) - w.WriteString(fmt.Sprintf("update add %s.%s %v %s %s\n", hostname, zone, ttl, addrType, ipAddr)) - w.WriteString("send\n") - - w.Flush() - f.Close() - - cmd := exec.Command("/usr/bin/nsupdate", f.Name()) - var out bytes.Buffer - var stderr bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = &stderr - err = cmd.Run() - if err != nil { - return fmt.Errorf("%v: %v", err, stderr.String()) - } - - if out.String() != "" { - return fmt.Errorf(out.String()) - } - - return nil -} - -func (h *Handler) deleteRecord(hostname string, zone string) error { - fmt.Printf("record delete request: %s\n", hostname) - - f, err := ioutil.TempFile(os.TempDir(), "dyndns") - if err != nil { - return err - } - - defer os.Remove(f.Name()) - w := bufio.NewWriter(f) - - w.WriteString(fmt.Sprintf("server %s\n", "localhost")) - w.WriteString(fmt.Sprintf("zone %s\n", zone)) - w.WriteString(fmt.Sprintf("update delete %s.%s\n", hostname, zone)) - w.WriteString("send\n") - - w.Flush() - f.Close() - - cmd := exec.Command("/usr/bin/nsupdate", f.Name()) - var out bytes.Buffer - var stderr bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = &stderr - err = cmd.Run() - if err != nil { - return fmt.Errorf("%v: %v", err, stderr.String()) - } - - if out.String() != "" { - return fmt.Errorf(out.String()) - } - - return nil -} - -func getIPType(ipAddr string) string { +// GetIPType finds out if the IP is IPv4 or IPv6 +func GetIPType(ipAddr string) string { if ipparser.ValidIP4(ipAddr) { return "A" } else if ipparser.ValidIP6(ipAddr) { @@ -97,7 +22,9 @@ func getIPType(ipAddr string) string { } } -func getCallerIP(r *http.Request) (string, error) { +// GetCallerIP searches for the "real" IP senders has actually. +// If its a private address we won't use it. +func GetCallerIP(r *http.Request) (string, error) { fmt.Println("request", r.Header) for _, h := range []string{"X-Real-Ip", "X-Forwarded-For"} { addresses := strings.Split(r.Header.Get(h), ",") @@ -117,7 +44,14 @@ func getCallerIP(r *http.Request) (string, error) { return "", errors.New("no match") } -//ipRange - a structure that holds the start and end of a range of ip addresses +// ShrinkUserAgent simply cuts the user agent information if its too long to display. +func ShrinkUserAgent(agent string) string { + agentParts := strings.Split(agent, " ") + + return agentParts[0] +} + +// ipRange - a structure that holds the start and end of a range of ip addresses type ipRange struct { start net.IP end net.IP @@ -173,9 +107,3 @@ func isPrivateSubnet(ipAddress net.IP) bool { } return false } - -func shrinkUserAgent(agent string) string { - agentParts := strings.Split(agent, " ") - - return agentParts[0] -} diff --git a/dyndns/nswrapper/update.go b/dyndns/nswrapper/update.go new file mode 100644 index 0000000..c30cb97 --- /dev/null +++ b/dyndns/nswrapper/update.go @@ -0,0 +1,85 @@ +package nswrapper + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" +) + +// UpdateRecord builds a nsupdate file and updates a record by executing it with nsupdate. +func UpdateRecord(hostname string, ipAddr string, addrType string, zone string, ttl int) error { + fmt.Printf("%s record update request: %s -> %s\n", addrType, hostname, ipAddr) + + f, err := ioutil.TempFile(os.TempDir(), "dyndns") + if err != nil { + return err + } + + defer os.Remove(f.Name()) + w := bufio.NewWriter(f) + + w.WriteString(fmt.Sprintf("server %s\n", "localhost")) + w.WriteString(fmt.Sprintf("zone %s\n", zone)) + w.WriteString(fmt.Sprintf("update delete %s.%s %s\n", hostname, zone, addrType)) + w.WriteString(fmt.Sprintf("update add %s.%s %v %s %s\n", hostname, zone, ttl, addrType, ipAddr)) + w.WriteString("send\n") + + w.Flush() + f.Close() + + cmd := exec.Command("/usr/bin/nsupdate", f.Name()) + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + err = cmd.Run() + if err != nil { + return fmt.Errorf("%v: %v", err, stderr.String()) + } + + if out.String() != "" { + return fmt.Errorf(out.String()) + } + + return nil +} + +// DeleteRecord builds a nsupdate file and deletes a record by executing it with nsupdate. +func DeleteRecord(hostname string, zone string) error { + fmt.Printf("record delete request: %s\n", hostname) + + f, err := ioutil.TempFile(os.TempDir(), "dyndns") + if err != nil { + return err + } + + defer os.Remove(f.Name()) + w := bufio.NewWriter(f) + + w.WriteString(fmt.Sprintf("server %s\n", "localhost")) + w.WriteString(fmt.Sprintf("zone %s\n", zone)) + w.WriteString(fmt.Sprintf("update delete %s.%s\n", hostname, zone)) + w.WriteString("send\n") + + w.Flush() + f.Close() + + cmd := exec.Command("/usr/bin/nsupdate", f.Name()) + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + err = cmd.Run() + if err != nil { + return fmt.Errorf("%v: %v", err, stderr.String()) + } + + if out.String() != "" { + return fmt.Errorf(out.String()) + } + + return nil +}