organise backend code / frontend combobox / edit allowed ips

This commit is contained in:
vx3r 2020-02-05 10:53:53 +09:00
parent 08faa8c53f
commit 727b5c4049
13 changed files with 476 additions and 327 deletions

View File

@ -30,6 +30,7 @@ The goal is to run Wg Gen Web in a container and WireGuard on host system.
* Generation of `wg0.conf` after any modification
* Dockerized
* Pretty cool look

![Screenshot](wg-gen-web_screenshot.png)

## Running

View File

@ -4,8 +4,8 @@ import (
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/skip2/go-qrcode"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/repository"
"net/http"
)

@ -41,7 +41,7 @@ func createClient(c *gin.Context) {
return
}

client, err := repository.CreateClient(&data)
client, err := core.CreateClient(&data)
if err != nil {
log.WithFields(log.Fields{
"err": err,
@ -56,7 +56,7 @@ func createClient(c *gin.Context) {
func readClient(c *gin.Context) {
id := c.Param("id")

client, err := repository.ReadClient(id)
client, err := core.ReadClient(id)
if err != nil {
log.WithFields(log.Fields{
"err": err,
@ -80,7 +80,7 @@ func updateClient(c *gin.Context) {
return
}

client, err := repository.UpdateClient(id, &data)
client, err := core.UpdateClient(id, &data)
if err != nil {
log.WithFields(log.Fields{
"err": err,
@ -95,7 +95,7 @@ func updateClient(c *gin.Context) {
func deleteClient(c *gin.Context) {
id := c.Param("id")

err := repository.DeleteClient(id)
err := core.DeleteClient(id)
if err != nil {
log.WithFields(log.Fields{
"err": err,
@ -108,7 +108,7 @@ func deleteClient(c *gin.Context) {
}

func readClients(c *gin.Context) {
clients, err := repository.ReadClients()
clients, err := core.ReadClients()
if err != nil {
log.WithFields(log.Fields{
"err": err,
@ -121,7 +121,7 @@ func readClients(c *gin.Context) {
}

func configClient(c *gin.Context) {
configData, err := repository.ReadClientConfig(c.Param("id"))
configData, err := core.ReadClientConfig(c.Param("id"))
if err != nil {
log.WithFields(log.Fields{
"err": err,
@ -153,7 +153,7 @@ func configClient(c *gin.Context) {
func emailClient(c *gin.Context) {
id := c.Param("id")

err := repository.EmailClient(id)
err := core.EmailClient(id)
if err != nil {
log.WithFields(log.Fields{
"err": err,
@ -166,7 +166,7 @@ func emailClient(c *gin.Context) {
}

func readServer(c *gin.Context) {
client, err := repository.ReadServer()
client, err := core.ReadServer()
if err != nil {
log.WithFields(log.Fields{
"err": err,
@ -189,7 +189,7 @@ func updateServer(c *gin.Context) {
return
}

client, err := repository.UpdateServer(&data)
client, err := core.UpdateServer(&data)
if err != nil {
log.WithFields(log.Fields{
"err": err,

View File

@ -1,12 +1,13 @@
package repository
package core

import (
"encoding/json"
"errors"
uuid "github.com/satori/go.uuid"
log "github.com/sirupsen/logrus"
"github.com/skip2/go-qrcode"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/storage"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/template"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"gopkg.in/gomail.v2"
@ -68,27 +69,27 @@ func CreateClient(client *model.Client) (*model.Client, error) {
ips = append(ips, ip)
}
client.Address = strings.Join(ips, ",")

client.Created = time.Now().UTC()
client.Updated = client.Created

err = serialize(client.Id, client)
err = storage.Serialize(client.Id, client)
if err != nil {
return nil, err
}

v, err := deserialize(client.Id)
v, err := storage.Deserialize(client.Id)
if err != nil {
return nil, err
}
client = v.(*model.Client)

return client, nil
// data modified, dump new config
return client, UpdateServerConfigWg()
}

// ReadClient client by id
func ReadClient(id string) (*model.Client, error) {
v, err := deserialize(id)
v, err := storage.Deserialize(id)
if err != nil {
return nil, err
}
@ -97,6 +98,81 @@ func ReadClient(id string) (*model.Client, error) {
return client, nil
}

// UpdateClient preserve keys
func UpdateClient(Id string, client *model.Client) (*model.Client, error) {
v, err := storage.Deserialize(Id)
if err != nil {
return nil, err
}
current := v.(*model.Client)

if current.Id != client.Id {
return nil, errors.New("records Id mismatch")
}
// keep keys
client.PrivateKey = current.PrivateKey
client.PublicKey = current.PublicKey
client.Updated = time.Now().UTC()

err = storage.Serialize(client.Id, client)
if err != nil {
return nil, err
}

v, err = storage.Deserialize(Id)
if err != nil {
return nil, err
}
client = v.(*model.Client)

// data modified, dump new config
return client, UpdateServerConfigWg()
}

// DeleteClient from disk
func DeleteClient(id string) error {
path := filepath.Join(os.Getenv("WG_CONF_DIR"), id)
err := os.Remove(path)
if err != nil {
return err
}

// data modified, dump new config
return UpdateServerConfigWg()
}

// ReadClients all clients
func ReadClients() ([]*model.Client, error) {
clients := make([]*model.Client, 0)

files, err := ioutil.ReadDir(filepath.Join(os.Getenv("WG_CONF_DIR")))
if err != nil {
return nil, err
}

for _, f := range files {
// clients file name is an uuid
_, err := uuid.FromString(f.Name())
if err == nil {
c, err := storage.Deserialize(f.Name())
if err != nil {
log.WithFields(log.Fields{
"err": err,
"path": f.Name(),
}).Error("failed to storage.Destorage.Serialize client")
} else {
clients = append(clients, c.(*model.Client))
}
}
}

sort.Slice(clients, func(i, j int) bool {
return clients[i].Created.After(clients[j].Created)
})

return clients, nil
}

// ReadClientConfig in wg format
func ReadClientConfig(id string) ([]byte, error) {
client, err := ReadClient(id)
@ -109,55 +185,12 @@ func ReadClientConfig(id string) ([]byte, error) {
return nil, err
}

configDataWg, err := util.DumpClient(client, server)
configDataWg, err := template.DumpClientWg(client, server)
if err != nil {
return nil, err
}

return configDataWg.Bytes(), nil
}

// UpdateClient preserve keys
func UpdateClient(Id string, client *model.Client) (*model.Client, error) {
v, err := deserialize(Id)
if err != nil {
return nil, err
}
current := v.(*model.Client)

if current.Id != client.Id {
return nil, errors.New("records Id mismatch")
}
// keep keys
client.PrivateKey = current.PrivateKey
client.PublicKey = current.PublicKey

client.Updated = time.Now().UTC()

err = serialize(client.Id, client)
if err != nil {
return nil, err
}

v, err = deserialize(Id)
if err != nil {
return nil, err
}
client = v.(*model.Client)

return client, nil
}

// DeleteClient from disk
func DeleteClient(id string) error {
path := filepath.Join(os.Getenv("WG_CONF_DIR"), id)
err := os.Remove(path)
if err != nil {
return err
}

// data modified, dump new config
return generateWgConfig()
return configDataWg, nil
}

// SendEmail to client
@ -203,7 +236,7 @@ func EmailClient(id string) error {
defer os.Remove(tmpfilePng.Name()) // clean up

// get email body
emailBody, err := util.DumpEmail(client, filepath.Base(tmpfilePng.Name()))
emailBody, err := template.DumpEmail(client, filepath.Base(tmpfilePng.Name()))
if err != nil {
return err
}
@ -224,7 +257,7 @@ func EmailClient(id string) error {
m.SetHeader("From", os.Getenv("SMTP_FROM"))
m.SetAddressHeader("To", client.Email, client.Name)
m.SetHeader("Subject", "WireGuard VPN Configuration")
m.SetBody("text/html", emailBody.String())
m.SetBody("text/html", string(emailBody))
m.Attach(tmpfileCfg.Name())
m.Embed(tmpfilePng.Name())

@ -235,178 +268,3 @@ func EmailClient(id string) error {

return nil
}

// ReadClients all clients
func ReadClients() ([]*model.Client, error) {
clients := make([]*model.Client, 0)

files, err := ioutil.ReadDir(filepath.Join(os.Getenv("WG_CONF_DIR")))
if err != nil {
return nil, err
}

for _, f := range files {
// clients file name is an uuid
_, err := uuid.FromString(f.Name())
if err == nil {
c, err := deserialize(f.Name())
if err != nil {
log.WithFields(log.Fields{
"err": err,
"path": f.Name(),
}).Error("failed to deserialize client")
} else {
clients = append(clients, c.(*model.Client))
}
}
}

sort.Slice(clients, func(i, j int) bool {
return clients[i].Created.After(clients[j].Created)
})

return clients, nil
}

// ReadServer object, create default one
func ReadServer() (*model.Server, error) {
if !util.FileExists(filepath.Join(os.Getenv("WG_CONF_DIR"), "server.json")) {
server := &model.Server{}

key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
}
server.PrivateKey = key.String()
server.PublicKey = key.PublicKey().String()

presharedKey, err := wgtypes.GenerateKey()
if err != nil {
return nil, err
}
server.PresharedKey = presharedKey.String()

server.Name = "Created with default values"
server.Endpoint = "wireguard.example.com:123"
server.ListenPort = 51820
server.Address = "fd9f:6666::10:6:6:1/112, 10.6.6.1/24"
server.Dns = "fd9f::10:0:0:2, 10.0.0.2"
server.PersistentKeepalive = 16
server.Created = time.Now().UTC()
server.Updated = server.Created

err = serialize("server.json", server)
if err != nil {
return nil, err
}
}

c, err := deserialize("server.json")
if err != nil {
return nil, err
}

return c.(*model.Server), nil
}

// UpdateServer keep private values from existing one
func UpdateServer(server *model.Server) (*model.Server, error) {
current, err := deserialize("server.json")
if err != nil {
return nil, err
}
server.PrivateKey = current.(*model.Server).PrivateKey
server.PublicKey = current.(*model.Server).PublicKey
server.PresharedKey = current.(*model.Server).PresharedKey

server.Updated = time.Now().UTC()

err = serialize("server.json", server)
if err != nil {
return nil, err
}

v, err := deserialize("server.json")
if err != nil {
return nil, err
}
server = v.(*model.Server)

return server, nil
}

// Write object to disk
func serialize(id string, c interface{}) error {
b, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}

err = util.WriteFile(filepath.Join(os.Getenv("WG_CONF_DIR"), id), b)
if err != nil {
return err
}

// data modified, dump new config
return generateWgConfig()
}

// Read client from disc
func deserializeClient(data []byte) (*model.Client, error) {
var c *model.Client
err := json.Unmarshal(data, &c)
if err != nil {
return nil, err
}

return c, nil
}

// Read server from disc
func deserializeServer(data []byte) (*model.Server, error) {
var c *model.Server
err := json.Unmarshal(data, &c)
if err != nil {
return nil, err
}

return c, nil
}
func deserialize(id string) (interface{}, error) {
path := filepath.Join(os.Getenv("WG_CONF_DIR"), id)

b, err := util.ReadFile(path)
if err != nil {
return nil, err
}
if id == "server.json" {
return deserializeServer(b)
}

return deserializeClient(b)
}

// Generate Wireguard interface configuration
func generateWgConfig() error {
clients, err := ReadClients()
if err != nil {
return err
}

server, err := ReadServer()
if err != nil {
return err
}

configDataWg, err := util.DumpServerWg(clients, server)
if err != nil {
return err
}

err = util.WriteFile(filepath.Join(os.Getenv("WG_CONF_DIR"), os.Getenv("WG_INTERFACE_NAME")), configDataWg.Bytes())
if err != nil {
return err
}

return nil
}

98
core/server.go Normal file
View File

@ -0,0 +1,98 @@
package core

import (
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/storage"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/template"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"os"
"path/filepath"
"time"
)

// ReadServer object, create default one
func ReadServer() (*model.Server, error) {
if !util.FileExists(filepath.Join(os.Getenv("WG_CONF_DIR"), "server.json")) {
server := &model.Server{}

key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
}
server.PrivateKey = key.String()
server.PublicKey = key.PublicKey().String()

presharedKey, err := wgtypes.GenerateKey()
if err != nil {
return nil, err
}
server.PresharedKey = presharedKey.String()

server.Name = "Created with default values"
server.Endpoint = "wireguard.example.com:123"
server.ListenPort = 51820
server.Address = "fd9f:6666::10:6:6:1/112, 10.6.6.1/24"
server.Dns = "fd9f::10:0:0:2, 10.0.0.2"
server.PersistentKeepalive = 16
server.Created = time.Now().UTC()
server.Updated = server.Created

err = storage.Serialize("server.json", server)
if err != nil {
return nil, err
}
}

c, err := storage.Deserialize("server.json")
if err != nil {
return nil, err
}

return c.(*model.Server), nil
}

// UpdateServer keep private values from existing one
func UpdateServer(server *model.Server) (*model.Server, error) {
current, err := storage.Deserialize("server.json")
if err != nil {
return nil, err
}
server.PrivateKey = current.(*model.Server).PrivateKey
server.PublicKey = current.(*model.Server).PublicKey
server.PresharedKey = current.(*model.Server).PresharedKey
server.Updated = time.Now().UTC()

err = storage.Serialize("server.json", server)
if err != nil {
return nil, err
}

v, err := storage.Deserialize("server.json")
if err != nil {
return nil, err
}
server = v.(*model.Server)

return server, UpdateServerConfigWg()
}

// UpdateServerConfigWg in wg format
func UpdateServerConfigWg() error {
clients, err := ReadClients()
if err != nil {
return err
}

server, err := ReadServer()
if err != nil {
return err
}

_, err = template.DumpServerWg(clients, server)
if err != nil {
return err
}

return nil
}

47
storage/file.go Normal file
View File

@ -0,0 +1,47 @@
package storage

import (
"encoding/json"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
"os"
"path/filepath"
)

// Serialize write interface to disk
func Serialize(id string, c interface{}) error {
b, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}

return util.WriteFile(filepath.Join(os.Getenv("WG_CONF_DIR"), id), b)
}

// Deserialize read interface from disk
func Deserialize(id string) (interface{}, error) {
path := filepath.Join(os.Getenv("WG_CONF_DIR"), id)

data, err := util.ReadFile(path)
if err != nil {
return nil, err
}

if id == "server.json" {
var s *model.Server
err = json.Unmarshal(data, &s)
if err != nil {
return nil, err
}
return s, nil
}

// if not the server, must be client
var c *model.Client
err = json.Unmarshal(data, &c)
if err != nil {
return nil, err
}

return c, nil
}

View File

@ -1,8 +1,11 @@
package util
package template

import (
"bytes"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
"os"
"path/filepath"
"strings"
"text/template"
)
@ -226,13 +229,11 @@ AllowedIPs = {{.Address}}
{{end}}`
)

// DumpClient dump client wg config with go template
func DumpClient(client *model.Client, server *model.Server) (bytes.Buffer, error) {
var tplBuff bytes.Buffer

// DumpClientWg dump client wg config with go template
func DumpClientWg(client *model.Client, server *model.Server) ([]byte, error) {
t, err := template.New("client").Parse(clientTpl)
if err != nil {
return tplBuff, err
return nil, err
}

return dump(t, struct {
@ -244,16 +245,14 @@ func DumpClient(client *model.Client, server *model.Server) (bytes.Buffer, error
})
}

// DumpServerWg dump server wg config with go template
func DumpServerWg(clients []*model.Client, server *model.Server) (bytes.Buffer, error) {
var tplBuff bytes.Buffer

// DumpServerWg dump server wg config with go template, write it to file and return bytes
func DumpServerWg(clients []*model.Client, server *model.Server) ([]byte, error) {
t, err := template.New("server").Parse(wgTpl)
if err != nil {
return tplBuff, err
return nil, err
}

return dump(t, struct {
configDataWg, err := dump(t, struct {
Clients []*model.Client
Server *model.Server
ServerAdresses []string
@ -262,15 +261,23 @@ func DumpServerWg(clients []*model.Client, server *model.Server) (bytes.Buffer,
Clients: clients,
Server: server,
})
if err != nil {
return nil, err
}

err = util.WriteFile(filepath.Join(os.Getenv("WG_CONF_DIR"), os.Getenv("WG_INTERFACE_NAME")), configDataWg)
if err != nil {
return nil, err
}

return configDataWg, nil
}

// DumpEmail dump server wg config with go template
func DumpEmail(client *model.Client, qrcodePngName string) (bytes.Buffer, error) {
var tplBuff bytes.Buffer

func DumpEmail(client *model.Client, qrcodePngName string) ([]byte, error) {
t, err := template.New("email").Parse(emailTpl)
if err != nil {
return tplBuff, err
return nil, err
}

return dump(t, struct {
@ -282,13 +289,13 @@ func DumpEmail(client *model.Client, qrcodePngName string) (bytes.Buffer, error)
})
}

func dump(tpl *template.Template, data interface{}) (bytes.Buffer, error) {
func dump(tpl *template.Template, data interface{}) ([]byte, error) {
var tplBuff bytes.Buffer

err := tpl.Execute(&tplBuff, data)
if err != nil {
return tplBuff, err
return nil, err
}

return tplBuff, nil
return tplBuff.Bytes(), nil
}

47
ui/package-lock.json generated
View File

@ -1453,6 +1453,14 @@
"tslib": "^1.9.0"
}
},
"cidr-regex": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-2.0.10.tgz",
"integrity": "sha512-sB3ogMQXWvreNPbJUZMRApxuRYd+KoIo4RGQ81VatjmMW6WJPo+IJZ2846FGItr9VzKo5w7DXzijPLGtSd0N3Q==",
"requires": {
"ip-regex": "^2.1.0"
}
},
"cipher-base": {
"version": "1.0.4",
"resolved": "https://registry.npm.taobao.org/cipher-base/download/cipher-base-1.0.4.tgz",
@ -3553,14 +3561,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -3575,20 +3581,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -3705,8 +3708,7 @@
"inherits": {
"version": "2.0.4",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -3718,7 +3720,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -3733,7 +3734,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -3741,14 +3741,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.9.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -3767,7 +3765,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -3857,8 +3854,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -3870,7 +3866,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -3992,7 +3987,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -4762,8 +4756,7 @@
"ip-regex": {
"version": "2.1.0",
"resolved": "https://registry.npm.taobao.org/ip-regex/download/ip-regex-2.1.0.tgz",
"integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
"dev": true
"integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk="
},
"ipaddr.js": {
"version": "1.9.0",
@ -4830,6 +4823,14 @@
"integrity": "sha1-9+RrWWiQRW23Tn9ul2yzJz0G+qs=",
"dev": true
},
"is-cidr": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-cidr/-/is-cidr-3.1.0.tgz",
"integrity": "sha512-3kxTForpuj8O4iHn0ocsn1jxRm5VYm60GDghK6HXmpn4IyZOoRy9/GmdjFA2yEMqw91TB1/K3bFTuI7FlFNR1g==",
"requires": {
"cidr-regex": "^2.0.10"
}
},
"is-color-stop": {
"version": "1.1.0",
"resolved": "https://registry.npm.taobao.org/is-color-stop/download/is-color-stop-1.1.0.tgz",

View File

@ -8,6 +8,7 @@
},
"dependencies": {
"axios": "^0.19.2",
"is-cidr": "^3.1.0",
"moment": "^2.24.0",
"vue": "^2.6.10",
"vue-moment": "^4.1.0",

View File

@ -4,6 +4,7 @@ import router from './router'
import vuetify from './plugins/vuetify';
import './plugins/axios';
import './plugins/moment';
import './plugins/cidr'

Vue.config.productionTip = false


11
ui/src/plugins/cidr.js Normal file
View File

@ -0,0 +1,11 @@
import Vue from 'vue'
const isCidr = require('is-cidr');

const plugin = {
install () {
Vue.isCidr = isCidr;
Vue.prototype.$isCidr = isCidr
}
};

Vue.use(plugin);

View File

@ -35,14 +35,26 @@
]"
required
/>
<v-text-field
<v-combobox
v-model="server.address"
chips
hint="Write IPv4 or IPv6 CIDR and hit enter"
label="Server interface addresses"
:rules="[
v => !!v || 'Server interface address is required',
]"
required
/>
multiple
dark
>
<template v-slot:selection="{ attrs, item, select, selected }">
<v-chip
v-bind="attrs"
:input-value="selected"
close
@click="select"
@click:close="server.address.splice(server.address.indexOf(item), 1)"
>
<strong>{{ item }}</strong>&nbsp;
</v-chip>
</template>
</v-combobox>
</v-col>
<v-col cols="6">
<v-text-field
@ -55,14 +67,6 @@
label="Preshared key"
disabled
/>
<v-text-field
v-model="server.dns"
label="DNS servers for clients"
:rules="[
v => !!v || 'DNS server is required',
]"
required
/>
<v-text-field
v-model="server.listenPort"
type="number"
@ -72,6 +76,26 @@
label="Listen port"
required
/>
<v-combobox
v-model="server.dns"
chips
hint="Write IPv4 or IPv6 address and hit enter"
label="DNS servers for clients"
multiple
dark
>
<template v-slot:selection="{ attrs, item, select, selected }">
<v-chip
v-bind="attrs"
:input-value="selected"
close
@click="select"
@click:close="server.dns.splice(server.dns.indexOf(item), 1)"
>
<strong>{{ item }}</strong>&nbsp;
</v-chip>
</template>
</v-combobox>
</v-col>
</div>

@ -99,7 +123,7 @@
</v-list-item-content>
<v-btn
color="success"
@click.stop="dialogAddClient = true"
@click.stop="startAddClient"
>
Add new client
<v-icon right dark>mdi-account-multiple-plus-outline</v-icon>
@ -112,7 +136,7 @@
cols="6"
>
<v-card
color="#1F7087"
:color="client.enable ? '#1F7087' : 'warning'"
class="mx-auto"
raised
shaped
@ -154,7 +178,7 @@
</v-btn>
<v-btn
text
@click.stop="dialogEditClient = true; clientToEdit = client"
@click.stop="editClient(client.id)"
>
Edit
<v-icon right dark>mdi-square-edit-outline</v-icon>
@ -181,7 +205,7 @@
v-on="on"
color="success"
v-model="client.enable"
v-on:change="updateClient(client)"
v-on:change="disableClient(client)"
/>
</template>
<span> {{client.enable ? 'Disable' : 'Enable'}} this client</span>
@ -195,6 +219,7 @@
</v-col>
</v-row>
<v-dialog
v-if="client"
v-model="dialogAddClient"
max-width="550"
>
@ -238,6 +263,27 @@
persistent-hint
required
/>
<v-combobox
v-model="client.allowedIPs"
chips
hint="Write IPv4 or IPv6 CIDR and hit enter"
label="Allowed IPs"
multiple
dark
>
<template v-slot:selection="{ attrs, item, select, selected }">
<v-chip
v-bind="attrs"
:input-value="selected"
close
@click="select"
@click:close="client.allowedIPs.splice(client.allowedIPs.indexOf(item), 1)"
>
<strong>{{ item }}</strong>&nbsp;
</v-chip>
</template>
</v-combobox>

<v-switch
v-model="client.enable"
color="red"
@ -253,7 +299,7 @@
<v-btn
:disabled="!valid"
color="success"
@click="addClient()"
@click="addClient(client)"
>
Submit
</v-btn>
@ -267,7 +313,7 @@
</v-card>
</v-dialog>
<v-dialog
v-if="clientToEdit"
v-if="client"
v-model="dialogEditClient"
max-width="550"
>
@ -283,22 +329,42 @@
v-model="valid"
>
<v-text-field
v-model="clientToEdit.name"
label="Client friendly name"
v-model="client.name"
label="Friendly name"
:rules="[
v => !!v || 'Client name is required',
]"
required
/>
<v-text-field
v-model="clientToEdit.email"
label="Client email"
v-model="client.email"
label="Email"
:rules="[
v => !!v || 'E-mail is required',
v => /.+@.+\..+/.test(v) || 'E-mail must be valid',
]"
v => !!v || 'Email is required',
v => /.+@.+\..+/.test(v) || 'Email must be valid',
]"
required
/>
<v-combobox
v-model="client.allowedIPs"
chips
hint="Write IPv4 or IPv6 CIDR and hit enter"
label="Allowed IPs"
multiple
dark
>
<template v-slot:selection="{ attrs, item, select, selected }">
<v-chip
v-bind="attrs"
:input-value="selected"
close
@click="select"
@click:close="client.allowedIPs.splice(client.allowedIPs.indexOf(item), 1)"
>
<strong>{{ item }}</strong>&nbsp;
</v-chip>
</template>
</v-combobox>
</v-form>
</v-col>
</v-row>
@ -308,7 +374,7 @@
<v-btn
:disabled="!valid"
color="success"
@click="updateClient(clientToEdit)"
@click="updateClient(client)"
>
Submit
</v-btn>
@ -361,22 +427,40 @@
serverAddress: [],
dialogAddClient: false,
dialogEditClient: false,
clientToEdit: null,
client: {
name: "",
email: "",
enable: true,
allowedIPs: "0.0.0.0/0,::/0",
address: "",
}
client: null,
}),

methods: {
startAddClient() {
this.dialogAddClient = true;
this.client = {
name: "",
email: "",
enable: true,
allowedIPs: ["0.0.0.0/0", "::/0"],
address: "",
}
},
editClient(id) {
this.$get(`/client/${id}`).then((res) => {
this.dialogEditClient = true;
res.allowedIPs = res.allowedIPs.split(',');
this.client = res
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
disableClient(client) {
client.allowedIPs = client.allowedIPs.split(',');
this.updateClient(client)
},
getData() {
this.$get('/server').then((res) => {
res.address = res.address.split(',');
res.dns = res.dns.split(',');
this.server = res;
this.clientAddress = this.serverAddress = this.server.address.split(',')
this.clientAddress = this.serverAddress = this.server.address
}).catch((e) => {
console.log(e)
this.notify('error', e.response.status + ' ' + e.response.statusText);
});

@ -390,6 +474,20 @@
// convert int values
this.server.listenPort = parseInt(this.server.listenPort, 10);
this.server.persistentKeepalive = parseInt(this.server.persistentKeepalive, 10);
// check server addresses
if (this.server.address.length < 1) {
this.notify('error', 'Please provide at least one valid CIDR address for server interface');
return;
}
for (let i = 0; i < this.server.address.length; i++){
if (this.$isCidr(this.server.address[i]) === 0) {
this.notify('error', 'Invalid CIDR detected, please correct before submitting');
return
}
}
this.server.address = this.server.address.join(',');
this.server.dns = this.server.dns.join(',');

this.$patch('/server', this.server).then((res) => {
this.notify('success', "Server successfully updated");
this.getData()
@ -397,10 +495,23 @@
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
addClient () {
addClient(client) {
if (client.allowedIPs.length < 1) {
this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
return;
}
for (let i = 0; i < client.allowedIPs.length; i++){
if (this.$isCidr(client.allowedIPs[i]) === 0) {
this.notify('error', 'Invalid CIDR detected, please correct before submitting');
return
}
}

this.dialogAddClient = false;
this.client.address = this.clientAddress.join(',');
this.$post('/client', this.client).then((res) => {
client.address = this.clientAddress.join(',');
client.allowedIPs = this.client.allowedIPs.join(',');

this.$post('/client', client).then((res) => {
this.notify('success', "Client successfully added");
this.getData()
}).catch((e) => {
@ -437,7 +548,20 @@
}
},
updateClient(client) {
if (client.allowedIPs.length < 1) {
this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
return;
}
for (let i = 0; i < client.allowedIPs.length; i++){
if (this.$isCidr(client.allowedIPs[i]) === 0) {
this.notify('error', 'Invalid CIDR detected, please correct before submitting');
return
}
}

this.dialogEditClient = false;
client.allowedIPs = client.allowedIPs.join(',');

this.$patch(`/client/${client.id}`, client).then((res) => {
this.notify('success', "Client successfully updated");
this.getData()

View File

@ -84,6 +84,11 @@ func GetAllAddressesFromCidr(cidr string) ([]string, error) {
return ips[2 : len(ips)-1], nil
}

// IsIPv6 check if given ip is IPv6
func IsIPv6(address string) bool {
return strings.Count(address, ":") >= 2
}

// http://play.golang.org/p/m8TNTtygK0
func inc(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
@ -93,8 +98,3 @@ func inc(ip net.IP) {
}
}
}

// IsIPv6 check if given ip is IPv6
func IsIPv6(address string) bool {
return strings.Count(address, ":") >= 2
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 153 KiB