This commit separates user credentials from resource record configs to allow user credentials to be used for multiple records instead of one.
137 lines
3.3 KiB
Go
137 lines
3.3 KiB
Go
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 "nohost", nil
|
|
}
|
|
|
|
// this should not fail as it is verified in LoadConfig, but better be sure
|
|
entry, ok := cfg.rrconfigs[hostname]
|
|
if !ok {
|
|
return "nohost", nil
|
|
}
|
|
|
|
// TODO: return notfqdn -> differentiate between 'hostname doesnt exist' and
|
|
// 'hostname is not fqdn'
|
|
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())
|
|
}
|
|
}
|