rewrite most code; removes database to solely rely on config file

This commit is contained in:
Thomas Preisner 2021-07-13 13:36:37 +02:00
parent 32bf03dc07
commit ef22e29cef
10 changed files with 246 additions and 249 deletions

37
auth.go
View file

@ -1,37 +0,0 @@
package main
import (
"golang.org/x/crypto/bcrypt"
"database/sql"
"fmt"
"net/http"
)
func authenticateUser(db *sql.DB, username, password string) bool {
hashedPassword, ok := getPasswordForUser(db, username)
if ok {
err := bcrypt.CompareHashAndPassword(hashedPassword, []byte(password))
//TODO: print error message?
return err == nil
} else {
return false
}
}
func basicAuth(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok || !authenticateUser(db, user, pass) {
w.Header().Set("WWW-Authenticate", `Basic realm="dyndns"`)
w.WriteHeader(401)
w.Write([]byte("badauth"))
return
}
userdata, err := getDataForUser(db, user)
if err != nil {
fmt.Println(err)
}
handleRequest(w, r, userdata)
}
}

View file

@ -1,68 +0,0 @@
package main
import (
"fmt"
"net"
"net/http"
"strings"
)
// check if net.ParseIP returns nil!!
func getIP(r *http.Request) net.IP {
addr := r.URL.Query().Get("myip")
if len(addr) > 0 {
return net.ParseIP(addr)
}
addr = r.Header.Get(http.CanonicalHeaderKey("X-Forwarded-For"))
if len(addr) > 0 {
end := strings.Index(addr, ", ")
if end == -1 {
end = len(addr)
}
return net.ParseIP(addr[:end])
}
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 {
panic(err)
}
return net.ParseIP(addr)
}
func handleRequest(w http.ResponseWriter, r *http.Request, data userData) {
msg := "good"
defer func() {
w.Write([]byte(msg))
}()
hostname := r.URL.Query().Get("hostname")
if len(hostname) <= 0 || hostname != data.hostname {
msg = "nohost"
return
}
ipaddr := getIP(r)
if ipaddr == nil {
//TODO:
msg = "error"
return
}
msg = "Hostname: " + hostname + " , IP: " + ipaddr.String()
updated, err := updateNameserver(data, ipaddr)
if err != nil {
msg = "dnserr"
fmt.Println(err)
return
}
if !updated {
msg = "nochg"
return
}
}

View file

@ -1,6 +1,9 @@
[database]
host = "127.0.0.1"
port = 5432 port = 5432
user = "username"
pass = "password" [[rrconfig]]
name = "database" username = "username"
password = "password"
nameserver = "ns1.example.org"
zonename = "example.org"
hostname = "somerecord.example.org"
tsigkey = "<tsig-key>"

48
config.go Normal file
View file

@ -0,0 +1,48 @@
package main
import (
"fmt"
"github.com/pelletier/go-toml"
"os"
)
type Config struct {
ServerPort uint16 `toml:"port"`
RRConfigs []RRConfig `toml:"rrconfig"`
rrconfigs map[string]*RRConfig
}
type RRConfig struct {
Username string `toml:"username"`
Password string `toml:"password"`
Nameserver string `toml:"nameserver"`
Zonename string `toml:"zonename"`
Hostname string `toml:"hostname"`
Tsigkey string `toml:"tsigkey"`
Ttl int `toml:"ttl" default:"60"`
}
func LoadConfig(path string) (*Config, error) {
var cfg Config
f, err := os.Open(path)
if err != nil {
return nil, err
}
decoder := toml.NewDecoder(f).Strict(true)
err = decoder.Decode(&cfg)
if err != nil {
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")
}
cfg.rrconfigs[entry.Username] = &entry
}
return &cfg, nil
}

72
data.go
View file

@ -1,72 +0,0 @@
package main
import (
"fmt"
"database/sql"
_ "github.com/lib/pq"
)
type databaseConfig struct {
Host string
Port int
User string
Pass string
Name string
}
type userData struct {
nameserver string
zonename string
hostname string
tsigkey string
}
func prepareDatabase() (*sql.DB, error) {
dbconf := config.Database
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s " +
"sslmode=disable", dbconf.Host, dbconf.Port,
dbconf.User, dbconf.Pass, dbconf.Name)
db, err := sql.Open("postgres", psqlInfo)
if err != nil {
return nil, err
}
_, err = db.Exec("CREATE TABLE IF NOT EXISTS users " +
"(id SERIAL PRIMARY KEY, username VARCHAR(255) NULL, " +
"password VARCHAR(255) NULL, salt VARCHAR(255) NULL, " +
"nameserver VARCHAR(255) NULL, zone VARCHAR(255) NULL, " +
"hostname VARCHAR(255) NULL, tsig VARCHAR(255) NULL)")
if err != nil {
db.Close()
return nil, err
}
return db, nil
}
func getPasswordForUser(db *sql.DB, username string) ([]byte, bool) {
var password []byte
row := db.QueryRow("SELECT password FROM users WHERE username=$1", username)
err := row.Scan(&password)
if err != nil {
if err == sql.ErrNoRows {
return nil, false
} else {
panic(err)
}
}
return password, true
}
func getDataForUser(db *sql.DB, username string) (userData, error) {
var data userData
row := db.QueryRow("SELECT nameserver, zone, hostname, tsig " +
"FROM users WHERE username=$1", username)
err := row.Scan(&data.nameserver, &data.zonename, &data.hostname,
&data.tsigkey)
if err != nil {
return data, err
}
return data, nil
}

8
go.mod Normal file
View file

@ -0,0 +1,8 @@
module git.preisner.eu/preisi/dyndns
go 1.16
require (
github.com/pelletier/go-toml v1.9.3
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
)

12
go.sum Normal file
View file

@ -0,0 +1,12 @@
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

20
main.go
View file

@ -2,34 +2,22 @@ package main
import ( import (
"flag" "flag"
"fmt"
"net/http" "net/http"
"github.com/BurntSushi/toml"
) )
type tomlConfig struct {
Database databaseConfig
}
var config tomlConfig
func main() { func main() {
var configPath string var configPath string
flag.StringVar(&configPath, "config", "config.toml", "path to config file") flag.StringVar(&configPath, "config", "config.toml", "path to config file")
flag.Parse() flag.Parse()
_, err := toml.DecodeFile(configPath, &config) cfg, err := LoadConfig(configPath)
if err != nil { if err != nil {
panic(err) panic(err)
} }
db, err := prepareDatabase() http.HandleFunc("/", RequestHandler(cfg))
if err != nil { err = http.ListenAndServe(fmt.Sprintf(":%d", cfg.ServerPort), nil)
panic(err)
}
defer db.Close()
http.HandleFunc("/", basicAuth(db))
err = http.ListenAndServe(":3002", nil)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View file

@ -1,70 +1,59 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"log"
"net" "net"
"os/exec" "os/exec"
"strings" "strings"
) )
func createQuery(data userData, addr net.IP, deleteRecord bool) string { func requiresRRUpdate(entry *RRConfig, addr net.IP) bool {
var query strings.Builder // TODO: use custom resolver to query authoritive nameserver instead of
// local resolver
query.WriteString(fmt.Sprintf("key %s\n", data.tsigkey)) addrs, err := net.LookupIP(entry.Hostname)
query.WriteString(fmt.Sprintf("server %s\n", data.nameserver)) if err != nil {
query.WriteString(fmt.Sprintf("zone %s\n", data.zonename)) log.Printf("dns lookup failed: %s", err)
if deleteRecord { // enforce update, it's better than not trying at all
query.WriteString(fmt.Sprintf("update delete %s. A\n", data.hostname)) return true
}
query.WriteString(fmt.Sprintf("update add %s. A %s\n", data.hostname,
addr.String()))
query.WriteString("show\n")
query.WriteString("send\n")
return query.String()
} }
func queryNameserver(query string) error { // check if the current ip matches
var ( for _, ip := range addrs {
stdout bytes.Buffer if ip.Equal(addr) {
stderr bytes.Buffer // the ip seems to be still up-to-date -> no update required
) return false
}
}
return true
}
func updateRR(entry *RRConfig, addr net.IP) error {
query := generateQuery(entry, addr)
return executeQuery(query)
}
func generateQuery(entry *RRConfig, addr net.IP) string {
var q strings.Builder
fmt.Fprintf(&q, "key %s\n", 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, "send\n")
return q.String()
}
func executeQuery(query string) error {
cmd := exec.Command("knsupdate", "-v") cmd := exec.Command("knsupdate", "-v")
cmd.Stdin = strings.NewReader(query) cmd.Stdin = strings.NewReader(query)
cmd.Stdout = &stdout output, err := cmd.CombinedOutput()
cmd.Stderr = &stderr // TODO: is outputting stdout+stderr on failure even necessary?
err := cmd.Run() if err != nil {
fmt.Printf(stdout.String()) log.Printf("executeQuery failed: %s", output)
fmt.Printf(stderr.String()) }
return err return err
} }
func updateNameserver(data userData, addr net.IP) (bool, error) {
updateRecord := true
deleteRecord := true
addrs, err := net.LookupIP(data.hostname)
if err != nil {
deleteRecord = false
} else {
for _, ip := range addrs {
if ip.Equal(addr) {
updateRecord = false
break
}
}
}
if !updateRecord {
return false, nil
} else {
query := createQuery(data, addr, deleteRecord)
err = queryNameserver(query)
if err != nil {
return true, err
}
return true, nil
}
}

126
web.go Normal file
View file

@ -0,0 +1,126 @@
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
"log"
"net"
"net/http"
"strings"
)
func isAuthenticated(cfg *Config, r *http.Request) *RRConfig {
user, pw, ok := r.BasicAuth()
if !ok {
// no basic auth header detected
return nil
}
entry, ok := cfg.rrconfigs[user]
if !ok {
// non-existent username
return nil
}
err := bcrypt.CompareHashAndPassword([]byte(entry.Password), []byte(pw))
if err != nil {
log.Printf("Config contains invalid password hash for user %q", user)
return nil
}
return entry
}
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
func verifyHostname(entry *RRConfig, hostname string) string {
if len(hostname) <= 0 {
return "nohost"
}
// 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"
}
return ""
}
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 {
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 := verifyHostname(userdata, hostname)
if response != "" {
fmt.Fprintln(w, response)
return
}
ipaddr := getIpAddress(r)
if ipaddr == nil {
fmt.Fprintln(w, "911")
return
}
if !requiresRRUpdate(userdata, ipaddr) {
fmt.Fprintf(w, "nochg %s\n", ipaddr.String())
return
}
err := updateRR(userdata, ipaddr)
if err != nil {
log.Printf("updating RR failed: %s", err)
fmt.Fprintln(w, "911")
return
}
fmt.Fprintf(w, "good %s\n", ipaddr.String())
}
}