From cc06019738fa57d303c343e5d7dc3b007c1150e3 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Sat, 7 Nov 2020 18:36:23 +0100 Subject: [PATCH] wip: create/update/... --- assets/tpl/admin_edit_client.html | 4 +- assets/tpl/admin_edit_interface.html | 3 +- assets/tpl/admin_index.html | 15 +- go.mod | 1 + internal/common/iputil.go | 22 ++- internal/server/core.go | 1 + internal/server/handlers.go | 227 ++++++++------------------- internal/server/usermanager.go | 86 +++++++--- 8 files changed, 164 insertions(+), 195 deletions(-) diff --git a/assets/tpl/admin_edit_client.html b/assets/tpl/admin_edit_client.html index 9dc09d5..d8ba9f4 100644 --- a/assets/tpl/admin_edit_client.html +++ b/assets/tpl/admin_edit_client.html @@ -34,10 +34,12 @@
+ +
- +
diff --git a/assets/tpl/admin_edit_interface.html b/assets/tpl/admin_edit_interface.html index 55328f8..e537525 100644 --- a/assets/tpl/admin_edit_interface.html +++ b/assets/tpl/admin_edit_interface.html @@ -30,11 +30,12 @@ +

Server's interface configuration

- +
diff --git a/assets/tpl/admin_index.html b/assets/tpl/admin_index.html index 97b14ce..7da4f5f 100644 --- a/assets/tpl/admin_index.html +++ b/assets/tpl/admin_index.html @@ -14,7 +14,7 @@ {{template "prt_nav.html" .}}
-

WireGuard VPN Administration

+

WireGuard VPN Administration

@@ -77,9 +77,16 @@
-
-

Current VPN Users

-
+
+
+

Current VPN Users

+
+
+ + M +
+
+
diff --git a/go.mod b/go.mod index 42d4331..d8456bd 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/gin-gonic/contrib v0.0.0-20201005132743-ca038bbf2944 github.com/gin-gonic/gin v1.6.3 github.com/go-ldap/ldap/v3 v3.2.4 + github.com/go-playground/validator/v10 v10.2.0 github.com/gorilla/sessions v1.2.1 // indirect github.com/kelseyhightower/envconfig v1.4.0 github.com/sirupsen/logrus v1.7.0 diff --git a/internal/common/iputil.go b/internal/common/iputil.go index 7595bb5..0241b02 100644 --- a/internal/common/iputil.go +++ b/internal/common/iputil.go @@ -1,6 +1,9 @@ package common -import "net" +import ( + "net" + "strings" +) // BroadcastAddr returns the last address in the given network, or the broadcast address. func BroadcastAddr(n *net.IPNet) net.IP { @@ -35,3 +38,20 @@ func IsIPv6(address string) bool { } return ip.To4() == nil } + +func ParseIPList(lst string) []string { + ips := strings.Split(lst, ",") + validatedIPs := make([]string, 0, len(ips)) + for i := range ips { + ips[i] = strings.TrimSpace(ips[i]) + if ips[i] != "" { + validatedIPs = append(validatedIPs, ips[i]) + } + } + + return validatedIPs +} + +func IPListToString(lst []string) string { + return strings.Join(lst, ", ") +} diff --git a/internal/server/core.go b/internal/server/core.go index 9b7063a..063ad53 100644 --- a/internal/server/core.go +++ b/internal/server/core.go @@ -38,6 +38,7 @@ type SessionData struct { Search string AlertData string AlertType string + FormData interface{} } type AlertData struct { diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 3577ccd..c8455e5 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -1,12 +1,15 @@ package server import ( + "crypto/md5" + "fmt" "net/http" "net/url" "strconv" - "strings" "time" + "github.com/h44z/wg-portal/internal/common" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/gin-gonic/gin" @@ -77,72 +80,22 @@ func (s *Server) GetAdminEditInterface(c *gin.Context) { } func (s *Server) PostAdminEditInterface(c *gin.Context) { - device := s.users.GetDevice() - var err error - - device.ListenPort, err = strconv.Atoi(c.PostForm("port")) - if err != nil { - s.setAlert(c, "invalid port: "+err.Error(), "danger") - c.Redirect(http.StatusSeeOther, "/admin/device/edit") - return - } - - ipField := c.PostForm("ip") - ips := strings.Split(ipField, ",") - validatedIPs := make([]string, 0, len(ips)) - for i := range ips { - ips[i] = strings.TrimSpace(ips[i]) - if ips[i] != "" { - validatedIPs = append(validatedIPs, ips[i]) - } - } - if len(validatedIPs) == 0 { - s.setAlert(c, "invalid ip address", "danger") - c.Redirect(http.StatusSeeOther, "/admin/device/edit") - return - } - device.IPs = validatedIPs - - device.Endpoint = c.PostForm("endpoint") - - dnsField := c.PostForm("dns") - dns := strings.Split(dnsField, ",") - validatedDNS := make([]string, 0, len(dns)) - for i := range dns { - dns[i] = strings.TrimSpace(dns[i]) - if dns[i] != "" { - validatedDNS = append(validatedDNS, dns[i]) - } - } - device.DNS = validatedDNS - - allowedIPField := c.PostForm("allowedip") - allowedIP := strings.Split(allowedIPField, ",") - validatedAllowedIP := make([]string, 0, len(allowedIP)) - for i := range allowedIP { - allowedIP[i] = strings.TrimSpace(allowedIP[i]) - if allowedIP[i] != "" { - validatedAllowedIP = append(validatedAllowedIP, allowedIP[i]) - } - } - device.AllowedIPs = validatedAllowedIP - - device.Mtu, err = strconv.Atoi(c.PostForm("mtu")) - if err != nil { - s.setAlert(c, "invalid MTU: "+err.Error(), "danger") - c.Redirect(http.StatusSeeOther, "/admin/device/edit") - return - } - - device.PersistentKeepalive, err = strconv.Atoi(c.PostForm("keepalive")) - if err != nil { - s.setAlert(c, "invalid PersistentKeepalive: "+err.Error(), "danger") + var formDevice Device + if err := c.ShouldBind(&formDevice); err != nil { + s.setAlert(c, "failed to bind form data: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/device/edit") return } + // Clean list input + formDevice.IPs = common.ParseIPList(formDevice.IPsStr) + formDevice.AllowedIPs = common.ParseIPList(formDevice.AllowedIPsStr) + formDevice.DNS = common.ParseIPList(formDevice.DNSStr) + formDevice.IPsStr = common.IPListToString(formDevice.IPs) + formDevice.AllowedIPsStr = common.IPListToString(formDevice.AllowedIPs) + formDevice.DNSStr = common.IPListToString(formDevice.DNS) // Update WireGuard device - err = s.wg.UpdateDevice(device.DeviceName, device.GetDeviceConfig()) + err := s.wg.UpdateDevice(formDevice.DeviceName, formDevice.GetDeviceConfig()) if err != nil { s.setAlert(c, "failed to update device in WireGuard: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/device/edit") @@ -150,7 +103,7 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) { } // Update in database - err = s.users.UpdateDevice(device) + err = s.users.UpdateDevice(formDevice) if err != nil { s.setAlert(c, "failed to update device in database: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/device/edit") @@ -183,77 +136,47 @@ func (s *Server) GetAdminEditPeer(c *gin.Context) { } func (s *Server) PostAdminEditPeer(c *gin.Context) { - user := s.users.GetUserByKey(c.Query("pkey")) + currentUser := s.users.GetUserByKey(c.Query("pkey")) urlEncodedKey := url.QueryEscape(c.Query("pkey")) - var err error - user.Identifier = c.PostForm("identifier") - if user.Identifier == "" { - s.setAlert(c, "invalid identifier, must not be empty", "danger") + var formUser User + if err := c.ShouldBind(&formUser); err != nil { + s.setAlert(c, "failed to bind form data: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) return } - user.Email = c.PostForm("mail") - if user.Email == "" { - s.setAlert(c, "invalid email, must not be empty", "danger") - c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) - return - } + // Clean list input + formUser.IPs = common.ParseIPList(formUser.IPsStr) + formUser.AllowedIPs = common.ParseIPList(formUser.AllowedIPsStr) + formUser.IPsStr = common.IPListToString(formUser.IPs) + formUser.AllowedIPsStr = common.IPListToString(formUser.AllowedIPs) - ipField := c.PostForm("ip") - ips := strings.Split(ipField, ",") - validatedIPs := make([]string, 0, len(ips)) - for i := range ips { - ips[i] = strings.TrimSpace(ips[i]) - if ips[i] != "" { - validatedIPs = append(validatedIPs, ips[i]) - } - } - if len(validatedIPs) == 0 { - s.setAlert(c, "invalid ip address", "danger") - c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) - return - } - user.IPs = validatedIPs - - allowedIPField := c.PostForm("allowedip") - allowedIP := strings.Split(allowedIPField, ",") - validatedAllowedIP := make([]string, 0, len(allowedIP)) - for i := range allowedIP { - allowedIP[i] = strings.TrimSpace(allowedIP[i]) - if allowedIP[i] != "" { - validatedAllowedIP = append(validatedAllowedIP, allowedIP[i]) - } - } - user.AllowedIPs = validatedAllowedIP - - user.IgnorePersistentKeepalive = c.PostForm("ignorekeepalive") != "" disabled := c.PostForm("isdisabled") != "" now := time.Now() - if disabled && user.DeactivatedAt == nil { - user.DeactivatedAt = &now + if disabled && currentUser.DeactivatedAt == nil { + formUser.DeactivatedAt = &now } else if !disabled { - user.DeactivatedAt = nil + formUser.DeactivatedAt = nil } // Update WireGuard device - if user.DeactivatedAt == &now { - err = s.wg.RemovePeer(user.PublicKey) + if formUser.DeactivatedAt == &now { + err := s.wg.RemovePeer(formUser.PublicKey) if err != nil { s.setAlert(c, "failed to remove peer in WireGuard: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) return } - } else if user.DeactivatedAt == nil && user.Peer != nil { - err = s.wg.UpdatePeer(user.GetPeerConfig()) + } else if formUser.DeactivatedAt == nil && currentUser.Peer != nil { + err := s.wg.UpdatePeer(formUser.GetPeerConfig()) if err != nil { s.setAlert(c, "failed to update peer in WireGuard: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) return } - } else if user.DeactivatedAt == nil && user.Peer == nil { - err = s.wg.AddPeer(user.GetPeerConfig()) + } else if formUser.DeactivatedAt == nil && currentUser.Peer == nil { + err := s.wg.AddPeer(formUser.GetPeerConfig()) if err != nil { s.setAlert(c, "failed to add peer in WireGuard: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) @@ -262,7 +185,7 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) { } // Update in database - err = s.users.UpdateUser(user) + err := s.users.UpdateUser(formUser) if err != nil { s.setAlert(c, "failed to update user in database: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) @@ -278,6 +201,20 @@ func (s *Server) GetAdminCreatePeer(c *gin.Context) { user := User{} user.AllowedIPsStr = device.AllowedIPsStr user.IPsStr = "" // TODO: add a valid ip here + psk, err := wgtypes.GenerateKey() + if err != nil { + s.HandleError(c, http.StatusInternalServerError, "Preshared key generation error", err.Error()) + return + } + key, err := wgtypes.GeneratePrivateKey() + if err != nil { + s.HandleError(c, http.StatusInternalServerError, "Private key generation error", err.Error()) + return + } + user.PresharedKey = psk.String() + user.PrivateKey = key.String() + user.PublicKey = key.PublicKey().String() + user.UID = fmt.Sprintf("u%x", md5.Sum([]byte(user.PublicKey))) c.HTML(http.StatusOK, "admin_edit_client.html", struct { Route string @@ -297,68 +234,28 @@ func (s *Server) GetAdminCreatePeer(c *gin.Context) { } func (s *Server) PostAdminCreatePeer(c *gin.Context) { - user := User{} - key, err := wgtypes.GeneratePrivateKey() - if err != nil { - s.HandleError(c, http.StatusInternalServerError, "Private key generation error", err.Error()) - return - } - user.PrivateKey = key.String() - user.PublicKey = key.PublicKey().String() - - user.Identifier = c.PostForm("identifier") - if user.Identifier == "" { - s.setAlert(c, "invalid identifier, must not be empty", "danger") + var formUser User + if err := c.ShouldBind(&formUser); err != nil { + s.setAlert(c, "failed to bind form data: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/peer/create") return } - user.Email = c.PostForm("mail") - if user.Email == "" { - s.setAlert(c, "invalid email, must not be empty", "danger") - c.Redirect(http.StatusSeeOther, "/admin/peer/create") - return - } + // Clean list input + formUser.IPs = common.ParseIPList(formUser.IPsStr) + formUser.AllowedIPs = common.ParseIPList(formUser.AllowedIPsStr) + formUser.IPsStr = common.IPListToString(formUser.IPs) + formUser.AllowedIPsStr = common.IPListToString(formUser.AllowedIPs) - ipField := c.PostForm("ip") - ips := strings.Split(ipField, ",") - validatedIPs := make([]string, 0, len(ips)) - for i := range ips { - ips[i] = strings.TrimSpace(ips[i]) - if ips[i] != "" { - validatedIPs = append(validatedIPs, ips[i]) - } - } - if len(validatedIPs) == 0 { - s.setAlert(c, "invalid ip address", "danger") - c.Redirect(http.StatusSeeOther, "/admin/peer/create") - return - } - user.IPs = validatedIPs - - allowedIPField := c.PostForm("allowedip") - allowedIP := strings.Split(allowedIPField, ",") - validatedAllowedIP := make([]string, 0, len(allowedIP)) - for i := range allowedIP { - allowedIP[i] = strings.TrimSpace(allowedIP[i]) - if allowedIP[i] != "" { - validatedAllowedIP = append(validatedAllowedIP, allowedIP[i]) - } - } - user.AllowedIPs = validatedAllowedIP - - user.IgnorePersistentKeepalive = c.PostForm("ignorekeepalive") != "" disabled := c.PostForm("isdisabled") != "" now := time.Now() - if disabled && user.DeactivatedAt == nil { - user.DeactivatedAt = &now - } else if !disabled { - user.DeactivatedAt = nil + if disabled { + formUser.DeactivatedAt = &now } // Update WireGuard device - if user.DeactivatedAt == nil { - err = s.wg.AddPeer(user.GetPeerConfig()) + if formUser.DeactivatedAt == nil { + err := s.wg.AddPeer(formUser.GetPeerConfig()) if err != nil { s.setAlert(c, "failed to add peer in WireGuard: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/peer/create") @@ -367,7 +264,7 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) { } // Update in database - err = s.users.CreateUser(user) + err := s.users.CreateUser(formUser) if err != nil { s.setAlert(c, "failed to add user in database: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/peer/create") diff --git a/internal/server/usermanager.go b/internal/server/usermanager.go index 5308e6f..cef6878 100644 --- a/internal/server/usermanager.go +++ b/internal/server/usermanager.go @@ -10,6 +10,10 @@ import ( "text/template" "time" + "github.com/gin-gonic/gin/binding" + + "github.com/go-playground/validator/v10" + "github.com/h44z/wg-portal/internal/wireguard" "github.com/h44z/wg-portal/internal/common" @@ -22,6 +26,42 @@ import ( "gorm.io/gorm" ) +// +// CUSTOM VALIDATORS ---------------------------------------------------------------------------- +// +var cidrList validator.Func = func(fl validator.FieldLevel) bool { + cidrListStr := fl.Field().String() + + cidrList := common.ParseIPList(cidrListStr) + for i := range cidrList { + _, _, err := net.ParseCIDR(cidrList[i]) + if err != nil { + return false + } + } + return true +} + +var ipList validator.Func = func(fl validator.FieldLevel) bool { + ipListStr := fl.Field().String() + + ipList := common.ParseIPList(ipListStr) + for i := range ipList { + ip := net.ParseIP(ipList[i]) + if ip == nil { + return false + } + } + return true +} + +func init() { + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + v.RegisterValidation("cidrlist", cidrList) + v.RegisterValidation("iplist", ipList) + } +} + // // USER ---------------------------------------------------------------------------------------- // @@ -31,19 +71,19 @@ type User struct { LdapUser *ldap.UserCacheHolderEntry `gorm:"-"` // optional, it is still possible to have users without ldap Config string `gorm:"-"` - UID string // uid for html identification + UID string `form:"uid" binding:"alphanum"` // uid for html identification IsOnline bool `gorm:"-"` - Identifier string // Identifier AND Email make a WireGuard peer unique - Email string `gorm:"index"` + Identifier string `form:"identifier" binding:"required,lt=64"` // Identifier AND Email make a WireGuard peer unique + Email string `gorm:"index" form:"mail" binding:"required,email"` - IgnorePersistentKeepalive bool - PresharedKey string - AllowedIPsStr string - IPsStr string + IgnorePersistentKeepalive bool `form:"ignorekeepalive"` + PresharedKey string `form:"presharedkey" binding:"omitempty,base64"` + AllowedIPsStr string `form:"allowedip" binding:"cidrlist"` + IPsStr string `form:"ip" binding:"cidrlist"` AllowedIPs []string `gorm:"-"` // IPs that are used in the client config file IPs []string `gorm:"-"` // The IPs of the client - PrivateKey string - PublicKey string `gorm:"primaryKey"` + PrivateKey string `form:"privkey" binding:"omitempty,base64"` + PublicKey string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"` DeactivatedAt *time.Time CreatedBy string @@ -128,23 +168,23 @@ func (u User) IsValid() bool { type Device struct { Interface *wgtypes.Device `gorm:"-"` - DeviceName string `gorm:"primaryKey"` - PrivateKey string - PublicKey string - PersistentKeepalive int - ListenPort int - Mtu int - Endpoint string - AllowedIPsStr string - IPsStr string + DeviceName string `form:"device" gorm:"primaryKey" binding:"required,alphanum"` + PrivateKey string `form:"privkey" binding:"base64"` + PublicKey string `form:"pubkey" binding:"required,base64"` + PersistentKeepalive int `form:"keepalive" binding:"gte=0"` + ListenPort int `form:"port" binding:"required,gt=0"` + Mtu int `form:"mtu" binding:"gte=0,lte=1500"` + Endpoint string `form:"endpoint" binding:"required,hostname_port"` + AllowedIPsStr string `form:"allowedip" binding:"cidrlist"` + IPsStr string `form:"ip" binding:"required,cidrlist"` AllowedIPs []string `gorm:"-"` // IPs that are used in the client config file IPs []string `gorm:"-"` // The IPs of the client - DNSStr string + DNSStr string `form:"dns" binding:"iplist"` DNS []string `gorm:"-"` // The DNS servers of the client - PreUp string - PostUp string - PreDown string - PostDown string + PreUp string `form:"preup"` + PostUp string `form:"postup"` + PreDown string `form:"predown"` + PostDown string `form:"postdown"` CreatedAt time.Time UpdatedAt time.Time }