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")