wip: create/update/...

This commit is contained in:
Christoph Haas 2020-11-07 18:36:23 +01:00
parent ea65e6b43c
commit cc06019738
8 changed files with 164 additions and 195 deletions

View File

@ -34,10 +34,12 @@
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="uid" value="{{.Peer.UID}}">
<input type="hidden" name="privkey" value="{{.Peer.PrivateKey}}">
<input type="hidden" name="presharedkey" value="{{.Peer.PresharedKey}}">
<div class="form-row">
<div class="form-group col-md-12">
<label for="inputServerPublicKey">Public Key</label>
<input type="text" name="pkey" disabled class="form-control" id="inputServerPublicKey" value="{{.Peer.PublicKey}}">
<input type="text" name="pubkey" readonly class="form-control" id="inputServerPublicKey" value="{{.Peer.PublicKey}}">
</div>
</div>
<div class="form-row">

View File

@ -30,11 +30,12 @@
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="privkey" value="{{.Device.PrivateKey}}">
<h3>Server's interface configuration</h3>
<div class="form-row">
<div class="form-group col-md-12">
<label for="inputServerPublicKey">Public Key</label>
<input type="text" name="pubkey" disabled class="form-control" id="inputServerPublicKey" value="{{.Device.PublicKey}}">
<input type="text" name="pubkey" readonly class="form-control" id="inputServerPublicKey" value="{{.Device.PublicKey}}">
</div>
</div>
<div class="form-row">

View File

@ -14,7 +14,7 @@
<body id="page-top">
{{template "prt_nav.html" .}}
<div class="container">
<h1>WireGuard VPN Administration</h1>
<h1 class="mt-2">WireGuard VPN Administration</h1>
<div class="card">
<div class="card-header">
@ -77,9 +77,16 @@
</div>
</div>
<div></div>
<h2>Current VPN Users</h2>
<div class="table-responsive">
<div class="mt-4 row">
<div class="col-sm-10 col-12">
<h2>Current VPN Users</h2>
</div>
<div class="col-sm-2 col-12">
<a href="/admin/peer/create" title="Add a LDAP user" class="btn btn-primary pull-right"><i class="fa fa-fw fa-user-plus"></i></a>
<a href="/admin/peer/create" title="Manual add a user" class="btn btn-primary pull-right"><i class="fa fa-fw fa-plus"></i>M</a>
</div>
</div>
<div class="mt-2 table-responsive">
<table class="table table-sm" id="userTable">
<thead>
<tr>

1
go.mod
View File

@ -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

View File

@ -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, ", ")
}

View File

@ -38,6 +38,7 @@ type SessionData struct {
Search string
AlertData string
AlertType string
FormData interface{}
}
type AlertData struct {

View File

@ -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")

View File

@ -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
}