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.
This commit is contained in:
parent
7f1500d53d
commit
451776bde0
4 changed files with 116 additions and 38 deletions
|
|
@ -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]]
|
[[rrconfig]]
|
||||||
username = "username"
|
|
||||||
password = "password"
|
|
||||||
nameserver = "ns1.example.org"
|
|
||||||
zonename = "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 = "<tsig-key>"
|
||||||
|
|
||||||
|
[[rrconfig]]
|
||||||
|
zonename = "example.org"
|
||||||
|
nameserver = "ns1.example.com"
|
||||||
|
hostname = "somerecord1.example.com"
|
||||||
|
tsig_algo = "some_algo"
|
||||||
|
tsig_id = "some_id"
|
||||||
|
tsigkey = "<tsig-key>"
|
||||||
|
|
||||||
|
[[rrconfig]]
|
||||||
|
zonename = "example.org"
|
||||||
|
nameserver = "ns1.example.org"
|
||||||
|
hostname = "somerecord2.example.org"
|
||||||
|
tsig_algo = "some_algo"
|
||||||
|
tsig_id = "some_id"
|
||||||
tsigkey = "<tsig-key>"
|
tsigkey = "<tsig-key>"
|
||||||
|
|
|
||||||
61
config.go
61
config.go
|
|
@ -8,18 +8,28 @@ import (
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ServerPort uint16 `toml:"port"`
|
ServerPort uint16 `toml:"port"`
|
||||||
|
Users []User `toml:"user"`
|
||||||
RRConfigs []RRConfig `toml:"rrconfig"`
|
RRConfigs []RRConfig `toml:"rrconfig"`
|
||||||
|
|
||||||
|
users map[string]*User
|
||||||
rrconfigs map[string]*RRConfig
|
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 {
|
type RRConfig struct {
|
||||||
Username string `toml:"username"`
|
Recordname string `toml:"recordname"`
|
||||||
Password string `toml:"password"`
|
|
||||||
Nameserver string `toml:"nameserver"`
|
|
||||||
Zonename string `toml:"zonename"`
|
Zonename string `toml:"zonename"`
|
||||||
Hostname string `toml:"hostname"`
|
Nameserver string `toml:"nameserver"`
|
||||||
Tsigkey string `toml:"tsigkey"`
|
Tsigalgo string `toml:"tsig_algo"`
|
||||||
|
Tsigid string `toml:"tsig_id"`
|
||||||
|
Tsigkey string `toml:"tsig_key"`
|
||||||
Ttl int `toml:"ttl" default:"60"`
|
Ttl int `toml:"ttl" default:"60"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,12 +47,43 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.rrconfigs = map[string]*RRConfig{}
|
// temporary map for preventing duplicate records between users
|
||||||
for _, entry := range cfg.RRConfigs {
|
records := make(map[string]bool)
|
||||||
if _, ok := cfg.rrconfigs[entry.Username]; ok {
|
|
||||||
return nil, fmt.Errorf("Duplicate username detected")
|
// 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
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
func requiresRRUpdate(entry *RRConfig, addr net.IP) bool {
|
func requiresRRUpdate(entry *RRConfig, addr net.IP) bool {
|
||||||
// TODO: use custom resolver to query authoritive nameserver instead of
|
// TODO: use custom resolver to query authoritive nameserver instead of
|
||||||
// local resolver
|
// local resolver
|
||||||
addrs, err := net.LookupIP(entry.Hostname)
|
addrs, err := net.LookupIP(entry.Recordname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("dns lookup failed: %s", err)
|
log.Printf("dns lookup failed: %s", err)
|
||||||
// enforce update, it's better than not trying at all
|
// 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 {
|
func generateQuery(entry *RRConfig, addr net.IP) string {
|
||||||
var q strings.Builder
|
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, "server %s\n", entry.Nameserver)
|
||||||
fmt.Fprintf(&q, "zone %s\n", entry.Zonename)
|
fmt.Fprintf(&q, "zone %s\n", entry.Zonename)
|
||||||
// TODO: check if addr is ipv4 or ipv6 (-> update A or AAAA)
|
// 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")
|
fmt.Fprintf(&q, "send\n")
|
||||||
|
|
||||||
return q.String()
|
return q.String()
|
||||||
|
|
|
||||||
51
web.go
51
web.go
|
|
@ -9,25 +9,25 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isAuthenticated(cfg *Config, r *http.Request) *RRConfig {
|
func isAuthenticated(cfg *Config, r *http.Request) *User {
|
||||||
user, pw, ok := r.BasicAuth()
|
username, password, ok := r.BasicAuth()
|
||||||
if !ok {
|
if !ok {
|
||||||
// no basic auth header detected
|
// no basic auth header detected
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, ok := cfg.rrconfigs[user]
|
user, ok := cfg.users[username]
|
||||||
if !ok {
|
if !ok {
|
||||||
// non-existent username
|
// non-existent username
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(entry.Password), []byte(pw))
|
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||||
if err != nil {
|
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 nil
|
||||||
}
|
}
|
||||||
return entry
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIpAddress(r *http.Request) net.IP {
|
func getIpAddress(r *http.Request) net.IP {
|
||||||
|
|
@ -61,25 +61,36 @@ func getIpAddress(r *http.Request) net.IP {
|
||||||
return net.ParseIP(addr)
|
return net.ParseIP(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns api-response on failure
|
// returns api-response on failure and RRConfig on success
|
||||||
func verifyHostname(entry *RRConfig, hostname string) string {
|
func verifyHostname(cfg *Config, user *User, hostname string) (string, *RRConfig) {
|
||||||
if len(hostname) <= 0 {
|
if len(hostname) <= 0 {
|
||||||
return "nohost"
|
return "nohost", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: allow single user to update multiple hostnames
|
// check whether the authenticated user is allowed to update the dns record
|
||||||
// TODO: return ntfqdn -> differentiate between 'hostname doesnt exist' and
|
_, ok := user.records[hostname]
|
||||||
// 'hostname is not fqdn'
|
if !ok {
|
||||||
if hostname != entry.Hostname {
|
return "nohost", nil
|
||||||
return "nohost"
|
|
||||||
}
|
}
|
||||||
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) {
|
func RequestHandler(cfg *Config) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userdata := isAuthenticated(cfg, r)
|
user := isAuthenticated(cfg, r)
|
||||||
if userdata == nil {
|
if user == nil {
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="dyndns"`)
|
w.Header().Set("WWW-Authenticate", `Basic realm="dyndns"`)
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
w.Write([]byte("badauth"))
|
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")
|
hostname := r.URL.Query().Get("hostname")
|
||||||
response := verifyHostname(userdata, hostname)
|
response, entry := verifyHostname(cfg, user, hostname)
|
||||||
if response != "" {
|
if response != "" {
|
||||||
fmt.Fprintln(w, response)
|
fmt.Fprintln(w, response)
|
||||||
return
|
return
|
||||||
|
|
@ -110,12 +121,12 @@ func RequestHandler(cfg *Config) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !requiresRRUpdate(userdata, ipaddr) {
|
if !requiresRRUpdate(entry, ipaddr) {
|
||||||
fmt.Fprintf(w, "nochg %s\n", ipaddr.String())
|
fmt.Fprintf(w, "nochg %s\n", ipaddr.String())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := updateRR(userdata, ipaddr)
|
err := updateRR(entry, ipaddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("updating RR failed: %s", err)
|
log.Printf("updating RR failed: %s", err)
|
||||||
fmt.Fprintln(w, "911")
|
fmt.Fprintln(w, "911")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue