From 451776bde0e998b7ad0956a794bd9f94f1104d85 Mon Sep 17 00:00:00 2001 From: Thomas Preisner Date: Wed, 8 Sep 2021 14:55:04 +0200 Subject: [PATCH] restructure config to allow user credentials to update multiple records This commit separates user credentials from resource record configs to allow user credentials to be used for multiple records instead of one. --- config.example.toml | 36 ++++++++++++++++++++++---- config.go | 61 +++++++++++++++++++++++++++++++++++++-------- nameserver.go | 6 ++--- web.go | 51 ++++++++++++++++++++++--------------- 4 files changed, 116 insertions(+), 38 deletions(-) diff --git a/config.example.toml b/config.example.toml index e2d5d24..af8b82c 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,9 +1,35 @@ -port = 5432 +port = 6000 + +[[user]] +username= "some_user" +password = "password1" +records = [ "somerecord1.example.org", "somerecord1.example.com" ] + +[[user]] +username= "other_user" +password = "password2" +records = [ "somerecord2.example.org" ] [[rrconfig]] -username = "username" -password = "password" -nameserver = "ns1.example.org" zonename = "example.org" -hostname = "somerecord.example.org" +nameserver = "ns1.example.org" +hostname = "somerecord1.example.org" +tsig_algo = "some_algo" +tsig_id = "some_id" +tsig_key = "" + +[[rrconfig]] +zonename = "example.org" +nameserver = "ns1.example.com" +hostname = "somerecord1.example.com" +tsig_algo = "some_algo" +tsig_id = "some_id" +tsigkey = "" + +[[rrconfig]] +zonename = "example.org" +nameserver = "ns1.example.org" +hostname = "somerecord2.example.org" +tsig_algo = "some_algo" +tsig_id = "some_id" tsigkey = "" diff --git a/config.go b/config.go index e845ba7..a114633 100644 --- a/config.go +++ b/config.go @@ -8,18 +8,28 @@ import ( type Config struct { ServerPort uint16 `toml:"port"` + Users []User `toml:"user"` RRConfigs []RRConfig `toml:"rrconfig"` + users map[string]*User rrconfigs map[string]*RRConfig } +type User struct { + Username string `toml:"username"` + Password string `toml:"password"` + Records []string `toml:"records"` + + records map[string]bool +} + type RRConfig struct { - Username string `toml:"username"` - Password string `toml:"password"` - Nameserver string `toml:"nameserver"` + Recordname string `toml:"recordname"` Zonename string `toml:"zonename"` - Hostname string `toml:"hostname"` - Tsigkey string `toml:"tsigkey"` + Nameserver string `toml:"nameserver"` + Tsigalgo string `toml:"tsig_algo"` + Tsigid string `toml:"tsig_id"` + Tsigkey string `toml:"tsig_key"` Ttl int `toml:"ttl" default:"60"` } @@ -37,12 +47,43 @@ func LoadConfig(path string) (*Config, error) { return nil, err } - cfg.rrconfigs = map[string]*RRConfig{} - for _, entry := range cfg.RRConfigs { - if _, ok := cfg.rrconfigs[entry.Username]; ok { - return nil, fmt.Errorf("Duplicate username detected") + // temporary map for preventing duplicate records between users + records := make(map[string]bool) + + // populate user map + cfg.users = map[string]*User{} + for _, user := range cfg.Users { + if _, ok := cfg.users[user.Username]; ok { + return nil, fmt.Errorf("Duplicate username detected: %s", user.Username) } - cfg.rrconfigs[entry.Username] = &entry + + // initialize and populate user.records map + user.records = make(map[string]bool) + if len(user.Records) <= 0 { + return nil, fmt.Errorf("User without records detected: %s", user.Username) + } + for _, record := range user.Records { + // check for duplicate records + if records[record] { + return nil, fmt.Errorf("Record associated with multiple users detected: %s", record) + } + // memorize record both in the user as well as in the temporary map + user.records[record] = true + records[record] = true + } + cfg.users[user.Username] = &user + } + + // populate record map + cfg.rrconfigs = map[string]*RRConfig{} + for _, record := range cfg.RRConfigs { + if !records[record.Recordname] { + return nil, fmt.Errorf("Record without associated user detected: %s", record.Recordname) + } + if _, ok := cfg.rrconfigs[record.Recordname]; ok { + return nil, fmt.Errorf("Duplicate record detected: %s", record.Recordname) + } + cfg.rrconfigs[record.Recordname] = &record } return &cfg, nil } diff --git a/nameserver.go b/nameserver.go index 1090331..642f770 100644 --- a/nameserver.go +++ b/nameserver.go @@ -11,7 +11,7 @@ import ( func requiresRRUpdate(entry *RRConfig, addr net.IP) bool { // TODO: use custom resolver to query authoritive nameserver instead of // local resolver - addrs, err := net.LookupIP(entry.Hostname) + addrs, err := net.LookupIP(entry.Recordname) if err != nil { log.Printf("dns lookup failed: %s", err) // enforce update, it's better than not trying at all @@ -36,11 +36,11 @@ func updateRR(entry *RRConfig, addr net.IP) error { func generateQuery(entry *RRConfig, addr net.IP) string { var q strings.Builder - fmt.Fprintf(&q, "key %s\n", entry.Tsigkey) + fmt.Fprintf(&q, "key %s:%s %s\n", entry.Tsigalgo, entry.Tsigid, entry.Tsigkey) fmt.Fprintf(&q, "server %s\n", entry.Nameserver) fmt.Fprintf(&q, "zone %s\n", entry.Zonename) // TODO: check if addr is ipv4 or ipv6 (-> update A or AAAA) - fmt.Fprintf(&q, "add %s. %d A %s\n", entry.Hostname, entry.Ttl, addr.String()) + fmt.Fprintf(&q, "add %s. %d A %s\n", entry.Recordname, entry.Ttl, addr.String()) fmt.Fprintf(&q, "send\n") return q.String() diff --git a/web.go b/web.go index 6a31952..a87cc90 100644 --- a/web.go +++ b/web.go @@ -9,25 +9,25 @@ import ( "strings" ) -func isAuthenticated(cfg *Config, r *http.Request) *RRConfig { - user, pw, ok := r.BasicAuth() +func isAuthenticated(cfg *Config, r *http.Request) *User { + username, password, ok := r.BasicAuth() if !ok { // no basic auth header detected return nil } - entry, ok := cfg.rrconfigs[user] + user, ok := cfg.users[username] if !ok { // non-existent username return nil } - err := bcrypt.CompareHashAndPassword([]byte(entry.Password), []byte(pw)) + err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) if err != nil { - log.Printf("Config contains invalid password hash for user %q", user) + log.Printf("Config contains invalid password hash for user %q", username) return nil } - return entry + return user } func getIpAddress(r *http.Request) net.IP { @@ -61,25 +61,36 @@ func getIpAddress(r *http.Request) net.IP { return net.ParseIP(addr) } -// returns api-response on failure -func verifyHostname(entry *RRConfig, hostname string) string { +// 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" + return "nohost", nil } - // TODO: allow single user to update multiple hostnames - // TODO: return ntfqdn -> differentiate between 'hostname doesnt exist' and - // 'hostname is not fqdn' - if hostname != entry.Hostname { - return "nohost" + // check whether the authenticated user is allowed to update the dns record + _, ok := user.records[hostname] + if !ok { + return "nohost", nil } - return "" + + // 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) { - userdata := isAuthenticated(cfg, r) - if userdata == nil { + user := isAuthenticated(cfg, r) + if user == nil { w.Header().Set("WWW-Authenticate", `Basic realm="dyndns"`) w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("badauth")) @@ -98,7 +109,7 @@ func RequestHandler(cfg *Config) func(w http.ResponseWriter, r *http.Request) { } hostname := r.URL.Query().Get("hostname") - response := verifyHostname(userdata, hostname) + response, entry := verifyHostname(cfg, user, hostname) if response != "" { fmt.Fprintln(w, response) return @@ -110,12 +121,12 @@ func RequestHandler(cfg *Config) func(w http.ResponseWriter, r *http.Request) { return } - if !requiresRRUpdate(userdata, ipaddr) { + if !requiresRRUpdate(entry, ipaddr) { fmt.Fprintf(w, "nochg %s\n", ipaddr.String()) return } - err := updateRR(userdata, ipaddr) + err := updateRR(entry, ipaddr) if err != nil { log.Printf("updating RR failed: %s", err) fmt.Fprintln(w, "911")