rewrite most code; removes database to solely rely on config file
This commit is contained in:
parent
32bf03dc07
commit
ef22e29cef
10 changed files with 246 additions and 249 deletions
37
auth.go
37
auth.go
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
68
backend.go
68
backend.go
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
[database]
|
||||
host = "127.0.0.1"
|
||||
port = 5432
|
||||
user = "username"
|
||||
pass = "password"
|
||||
name = "database"
|
||||
|
||||
[[rrconfig]]
|
||||
username = "username"
|
||||
password = "password"
|
||||
nameserver = "ns1.example.org"
|
||||
zonename = "example.org"
|
||||
hostname = "somerecord.example.org"
|
||||
tsigkey = "<tsig-key>"
|
||||
|
|
|
|||
48
config.go
Normal file
48
config.go
Normal 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
72
data.go
|
|
@ -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
8
go.mod
Normal 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
12
go.sum
Normal 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
20
main.go
|
|
@ -2,34 +2,22 @@ package main
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type tomlConfig struct {
|
||||
Database databaseConfig
|
||||
}
|
||||
|
||||
var config tomlConfig
|
||||
|
||||
func main() {
|
||||
var configPath string
|
||||
flag.StringVar(&configPath, "config", "config.toml", "path to config file")
|
||||
flag.Parse()
|
||||
|
||||
_, err := toml.DecodeFile(configPath, &config)
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
db, err := prepareDatabase()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
http.HandleFunc("/", basicAuth(db))
|
||||
err = http.ListenAndServe(":3002", nil)
|
||||
http.HandleFunc("/", RequestHandler(cfg))
|
||||
err = http.ListenAndServe(fmt.Sprintf(":%d", cfg.ServerPort), nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,70 +1,59 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func createQuery(data userData, addr net.IP, deleteRecord bool) string {
|
||||
var query strings.Builder
|
||||
|
||||
query.WriteString(fmt.Sprintf("key %s\n", data.tsigkey))
|
||||
query.WriteString(fmt.Sprintf("server %s\n", data.nameserver))
|
||||
query.WriteString(fmt.Sprintf("zone %s\n", data.zonename))
|
||||
if deleteRecord {
|
||||
query.WriteString(fmt.Sprintf("update delete %s. A\n", data.hostname))
|
||||
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)
|
||||
if err != nil {
|
||||
log.Printf("dns lookup failed: %s", err)
|
||||
// enforce update, it's better than not trying at all
|
||||
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()
|
||||
// check if the current ip matches
|
||||
for _, ip := range addrs {
|
||||
if ip.Equal(addr) {
|
||||
// the ip seems to be still up-to-date -> no update required
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func queryNameserver(query string) error {
|
||||
var (
|
||||
stdout bytes.Buffer
|
||||
stderr bytes.Buffer
|
||||
)
|
||||
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.Stdin = strings.NewReader(query)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
fmt.Printf(stdout.String())
|
||||
fmt.Printf(stderr.String())
|
||||
output, err := cmd.CombinedOutput()
|
||||
// TODO: is outputting stdout+stderr on failure even necessary?
|
||||
if err != nil {
|
||||
log.Printf("executeQuery failed: %s", output)
|
||||
}
|
||||
|
||||
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
126
web.go
Normal 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())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue