From fbf10f9d1bcd3421d99e203a135ef82aacba2dd9 Mon Sep 17 00:00:00 2001 From: Torben Nehmer Date: Mon, 23 Aug 2021 20:41:32 +0200 Subject: [PATCH] Next implementation steps - Simplified UpdateRequest to the minimum required - renamed test user to example - removed user property from yml config, it is given by the file name. - splitted server code analogous to command line code so that each handler has its own file. - made viper confgi in userconfig internal - added validation to userconfig - added helper to combaine an IID with a v6net in userconfig --- service/userconfig.go | 81 ++++++++++++++++++++++++++++++--- users/{test.yml => example.yml} | 1 - webapi/server.go | 53 +++------------------ webapi/update.go | 58 +++++++++++++++++++++++ webapi/updaterequest.go | 27 +++-------- 5 files changed, 146 insertions(+), 74 deletions(-) rename users/{test.yml => example.yml} (96%) create mode 100644 webapi/update.go diff --git a/service/userconfig.go b/service/userconfig.go index dfb9968..2c9464b 100644 --- a/service/userconfig.go +++ b/service/userconfig.go @@ -2,8 +2,10 @@ package service import ( "encoding/json" + "errors" "fmt" "log" + "net" "os" "path" @@ -18,7 +20,7 @@ func (uae UnauthorizedError) Error() string { } type UserConfig struct { - DB *viper.Viper + db *viper.Viper UserName string PassWord string @@ -62,16 +64,22 @@ func LoadConfigForUser(username string, password string) (*UserConfig, error) { return nil, fmt.Errorf("failed to parse config file %s: %v", configFile, err) } - result := &UserConfig{DB: v} + result := &UserConfig{db: v, UserName: username} + + err = result.db.Unmarshal(result) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal config file %s: %v", configFile, err) + } err = result.PasswordCheck(password) if err != nil { + log.Printf("Failed to check password") return nil, UnauthorizedError("pwcheck failed") } - err = result.DB.Unmarshal(result) + err = result.Validate() if err != nil { - return nil, fmt.Errorf("failed to unmarshal config file %s: %v", configFile, err) + return nil, fmt.Errorf("failed to parse config: %v", err) } return result, nil @@ -85,8 +93,8 @@ func HashPassword(pw []byte) (string, error) { return string(hash), nil } -func (ur *UserConfig) PasswordCheck(pwToCheck string) error { - hashedPassword := []byte(ur.DB.GetString("password")) +func (uc *UserConfig) PasswordCheck(pwToCheck string) error { + hashedPassword := []byte(uc.PassWord) bytePwToCheck := []byte(pwToCheck) err := bcrypt.CompareHashAndPassword(hashedPassword, bytePwToCheck) @@ -94,6 +102,67 @@ func (ur *UserConfig) PasswordCheck(pwToCheck string) error { return err } +func (uco *UserConfigOther) ConvertIIDToAddress(localNet *net.IPNet) net.IP { + if localNet == nil { + return nil + } + out := make(net.IP, net.IPv6len) + ipiid := net.ParseIP(uco.V6IID) + for i := 0; i < net.IPv6len; i++ { + // We take the corresponding byte from the IID and mask it out with the + // inversed Mask of the network we got (in essence a Host Mask). This + // leaves us those bits, that are not taken by the netmask, so that we + // can OR all this together. + maskedIID := ipiid[i] &^ localNet.Mask[i] + out[i] = localNet.IP[i] | maskedIID + } + return out +} + +func (ucn *UserConfigNFT) ValidateSetNames() bool { + if ucn.Set4 == "" && ucn.Set6 == "" { + return true + } + return ucn.Set4 != ucn.Set6 +} + +func (uc *UserConfig) Validate() error { + if !uc.Router.NFT.ValidateSetNames() { + return errors.New("router NFT set names invalid (probably identical for v4 and v6)") + } + if uc.Router.DNS == "" { + return errors.New("router record has no DNS") + } + + dnsnames := make(map[string]bool) + dnsnames[uc.Router.DNS] = true + + for i, other := range uc.Others { + if other.DNS == "" { + return fmt.Errorf("other record #%d has no DNS", i) + } + if dnsnames[other.DNS] { + return fmt.Errorf("the DNS FQDN %s is used twice", other.DNS) + } + dnsnames[other.DNS] = true + if !other.NFT.ValidateSetNames() { + return fmt.Errorf("other %s NFT set names invalid (probably identical for v4 and v6)", other.DNS) + } + if other.V6IID == "" { + return fmt.Errorf("other record %s has no V6IID", other.DNS) + } + iidIP := net.ParseIP(other.V6IID) + if iidIP == nil { + return fmt.Errorf("other record %s has invalid V6IID %s", other.DNS, other.V6IID) + } + if iidIP.To4() != nil { + return fmt.Errorf("other record %s IID looks like an IPv4 Address", other.DNS) + } + } + + return nil +} + func (uc *UserConfig) PrettyPrint() string { s, err := json.MarshalIndent(uc, "", " ") if err != nil { diff --git a/users/test.yml b/users/example.yml similarity index 96% rename from users/test.yml rename to users/example.yml index c04df9e..86a20a2 100644 --- a/users/test.yml +++ b/users/example.yml @@ -1,4 +1,3 @@ -username: test password: $2a$10$ymHFfGjKINWsUKwEo5xp.efN6DR6NkiRCXsbH4VWo0Nnzma8Yc.82 router: DNS: brandfeld.dyn.local diff --git a/webapi/server.go b/webapi/server.go index e1b3e34..e85678d 100644 --- a/webapi/server.go +++ b/webapi/server.go @@ -5,19 +5,18 @@ import ( "log" "net/http" - "gitea.nehmer.net/torben/dyndns/service" "github.com/gorilla/mux" ) -func Server() { - r := mux.NewRouter() - r.StrictSlash(true) +var router = mux.NewRouter() - r.HandleFunc("/hello", handleHello) - r.HandleFunc("/update", handleUpdate) +func Server() { + router.StrictSlash(true) + + router.HandleFunc("/hello", handleHello) log.Printf("Listening to: %s", C.ListenAddress) - go log.Fatal(http.ListenAndServe(C.ListenAddress, r)) + go log.Fatal(http.ListenAndServe(C.ListenAddress, router)) } func handleHello(w http.ResponseWriter, r *http.Request) { @@ -28,43 +27,3 @@ func handleHello(w http.ResponseWriter, r *http.Request) { log.Println(r.Form) fmt.Fprint(w, "

Hello World

") } - -func handleUpdate(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - ur, err := createUpdateRequestFromForm(r.Form) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - uc, err := service.LoadConfigForUser(ur.UserName, ur.Password) - if err != nil { - if _, ok := err.(*service.UnauthorizedError); ok { - http.Error(w, err.Error(), http.StatusUnauthorized) - } else { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - return - } - v := uc.DB - - w.Header().Set("Content-Type", "text/plain") - fmt.Fprintln(w, "OK") - fmt.Fprintln(w, ur.PrettyPrint()) - log.Println(ur.PrettyPrint()) - log.Println(ur.IPv6Net.IP) - log.Println(ur.IPv6Net.Mask) - log.Println(v.AllSettings()) - log.Printf("Request PW: %s, Config PW: %s", ur.Password, v.GetString("password")) - fmt.Fprintln(w, "Unmarshalled userconfig:") - fmt.Fprintln(w, uc.PrettyPrint()) - fmt.Fprintln(w, "WebAPI Config:") - fmt.Fprintln(w, C.PrettyPrint()) - fmt.Fprintln(w, "Service Config:") - fmt.Fprintln(w, service.C.PrettyPrint()) -} diff --git a/webapi/update.go b/webapi/update.go new file mode 100644 index 0000000..2130226 --- /dev/null +++ b/webapi/update.go @@ -0,0 +1,58 @@ +package webapi + +import ( + "fmt" + "log" + "net/http" + + "gitea.nehmer.net/torben/dyndns/service" +) + +func init() { + router.HandleFunc("/update", handleUpdate) +} + +func handleUpdate(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + ur, err := createUpdateRequestFromForm(r.Form) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + uc, err := service.LoadConfigForUser(ur.UserName, ur.Password) + if err != nil { + if _, ok := err.(*service.UnauthorizedError); ok { + http.Error(w, err.Error(), http.StatusUnauthorized) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + log.Printf("Authorization failed: %v", err) + return + } + + w.Header().Set("Content-Type", "text/plain") + fmt.Fprintln(w, "OK") + fmt.Fprintln(w, ur.PrettyPrint()) + log.Println(ur.PrettyPrint()) + log.Println(ur.IPv6Net.IP) + log.Println(ur.IPv6Net.Mask) + log.Printf("Request PW: %s, Config PW: %s", ur.Password, uc.PassWord) + fmt.Fprintln(w, "Unmarshalled userconfig:") + fmt.Fprintln(w, uc.PrettyPrint()) + fmt.Fprintln(w, "WebAPI Config:") + fmt.Fprintln(w, C.PrettyPrint()) + fmt.Fprintln(w, "Service Config:") + fmt.Fprintln(w, service.C.PrettyPrint()) + + for _, other := range uc.Others { + log.Printf("Other %s, IID %v, UR V6NEt: %v, merged: %v", + other.DNS, other.V6IID, ur.IPv6Net, + other.ConvertIIDToAddress(ur.IPv6Net)) + } +} diff --git a/webapi/updaterequest.go b/webapi/updaterequest.go index 72fbd3c..56bd722 100644 --- a/webapi/updaterequest.go +++ b/webapi/updaterequest.go @@ -10,18 +10,16 @@ import ( ) type UpdateRequest struct { - IPv4 net.IP - IPv6 net.IP - UserName string - Password string - Domain string - DualStack bool - IPv6Net *net.IPNet + IPv4 net.IP + IPv6 net.IP + UserName string + Password string + IPv6Net *net.IPNet } func (ur *UpdateRequest) String() string { - return fmt.Sprintf("IPv4: %v, IPv6: %v, UserName: %v, Password: %v, Domain: %v, DualStack: %v, IPv6Net: %v", - ur.IPv4, ur.IPv6, ur.UserName, ur.Password, ur.Domain, ur.DualStack, ur.IPv6Net) + return fmt.Sprintf("IPv4: %v, IPv6: %v, UserName: %v, Password: %v, IPv6Net: %v", + ur.IPv4, ur.IPv6, ur.UserName, ur.Password, ur.IPv6Net) } func (ur *UpdateRequest) PrettyPrint() string { @@ -57,17 +55,6 @@ func createUpdateRequestFromForm(form url.Values) (*UpdateRequest, error) { return nil, errors.New("a Password must be specified") } - ur.Domain = form.Get("Domain") - if ur.Domain == "" { - return nil, errors.New("a Domain must be specified") - } - - if form.Get("DualStack") == "1" { - ur.DualStack = true - } else { - ur.DualStack = false - } - if ip6net := form.Get("IPv6Net"); ip6net != "" { _, ipnet, err := net.ParseCIDR(ip6net) if err != nil {