package main import ( "fmt" "golang.org/x/crypto/bcrypt" "log" "net" "net/http" "strings" ) func isAuthenticated(cfg *Config, r *http.Request) *User { username, password, ok := r.BasicAuth() if !ok { // no basic auth header detected return nil } user, ok := cfg.users[username] if !ok { // non-existent username return nil } err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) if err != nil { log.Printf("Config contains invalid password hash for user %q", username) return nil } return user } func getIpAddress(r *http.Request) net.IP { addr := r.URL.Query().Get("myip") if len(addr) > 0 { // If myip was supplied, parse it and return the result regardless of // success, failure will be handled later. return net.ParseIP(addr) } // check http headers and request for other possible ip address sources addr = r.Header.Get(http.CanonicalHeaderKey("X-Forwarded-For")) if len(addr) > 0 { tokens := strings.Split(addr, ", ") // tokens is always at least 1 element long // -> return first element as it contains the client's ip address return net.ParseIP(tokens[0]) } // TODO: support newer standarized "Forwarded"-Header addr = r.Header.Get(http.CanonicalHeaderKey("X-Real-IP")) if len(addr) > 0 { return net.ParseIP(addr) } addr, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { log.Printf("Retrieving IP-Address failed: %s", err) return nil } return net.ParseIP(addr) } // returns api-response on failure and RRConfig on success func verifyHostname(cfg *Config, user *User, hostname string) (string, *RRConfig) { if len(hostname) <= 0 { return "nohost", nil } // check whether the authenticated user is allowed to update the dns record _, ok := user.records[hostname] if !ok { return "badauth", nil } // this should not fail as it is verified in LoadConfig, but better be safe entry, ok := cfg.rrconfigs[hostname] if !ok { return "nohost", nil } // TODO: return notfqdn -> differentiate between 'hostname doesnt exist' and // 'hostname is not fqdn' // again, this should not fail since 'hostname' was the key used for // cfg.rrconfigs to acquire the entry if hostname != entry.Recordname { return "nohost", nil } return "", entry } func RequestHandler(cfg *Config) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { user := isAuthenticated(cfg, r) if user == nil { w.Header().Set("WWW-Authenticate", `Basic realm="dyndns"`) w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("badauth")) fmt.Fprintln(w, "badauth") return } // all remaining responses should come with status code 200 w.WriteHeader(http.StatusOK) // check for 'valid' useragent, should not be strictly necessary but // the protocol demands it if len(r.UserAgent()) <= 0 { fmt.Fprintln(w, "badagent") return } hostname := r.URL.Query().Get("hostname") response, entry := verifyHostname(cfg, user, hostname) if response != "" { fmt.Fprintln(w, response) return } ipaddr := getIpAddress(r) if ipaddr == nil { fmt.Fprintln(w, "911") return } if !requiresRRUpdate(entry, ipaddr) { fmt.Fprintf(w, "nochg %s\n", ipaddr.String()) return } err := updateRR(entry, ipaddr) if err != nil { log.Printf("updating RR failed: %s", err) fmt.Fprintln(w, "911") return } fmt.Fprintf(w, "good %s\n", ipaddr.String()) } }