mirror of
https://github.com/vx3r/wg-gen-web.git
synced 2024-12-18 00:13:23 +00:00
Initial commit
This commit is contained in:
commit
024d2b4ebb
6
.env
Normal file
6
.env
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
SERVER=0.0.0.0
|
||||||
|
PORT=8080
|
||||||
|
GIN_MODE=debug
|
||||||
|
|
||||||
|
WG_CONF_DIR=./wireguard
|
||||||
|
WG_INTERFACE_NAME=wg0.conf
|
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Vendor
|
||||||
|
vendor/*
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
.idea/*
|
||||||
|
|
||||||
|
wireguard
|
||||||
|
|
||||||
|
ui/.idea/*
|
||||||
|
ui/dist/*
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
ui/node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
ui/.env.local
|
||||||
|
ui/.env.*.local
|
||||||
|
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
35
.gitlab-ci.yml
Normal file
35
.gitlab-ci.yml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
build-back:
|
||||||
|
stage: build
|
||||||
|
image: golang:latest
|
||||||
|
script:
|
||||||
|
- GOOS=linux GOARCH=amd64 go build -o ${CI_PROJECT_NAME}-linux-amd64
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- ${CI_PROJECT_NAME}-linux-amd64
|
||||||
|
|
||||||
|
build-front:
|
||||||
|
stage: build
|
||||||
|
image: node:10-alpine
|
||||||
|
script:
|
||||||
|
- cd ./ui
|
||||||
|
- npm install
|
||||||
|
- npm run build
|
||||||
|
- cd ..
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- ui/dist
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
stage: deploy
|
||||||
|
image: docker:latest
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
script:
|
||||||
|
- docker login -u ${CI_REGISTRY_USER} -p ${REGISTRY_PASSWORD} ${CI_REGISTRY}
|
||||||
|
- docker build --tag ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} --tag ${CI_REGISTRY_IMAGE}:latest .
|
||||||
|
- docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
|
||||||
|
- docker push ${CI_REGISTRY_IMAGE}:latest
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
FROM debian:stable-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ADD wg-gen-web-linux-amd64 .
|
||||||
|
ADD .env .
|
||||||
|
ADD ui/dist ui/dist
|
||||||
|
|
||||||
|
RUN chmod +x ./wg-gen-web-linux-amd64
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["/app/wg-gen-web-linux-amd64"]
|
13
LICENSE-WTFPL
Normal file
13
LICENSE-WTFPL
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 2, December 2004
|
||||||
|
|
||||||
|
Copyright (C) 2013 Stephen Mathieson <me@stephenmathieson.com>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
copies of this license document, and changing it is allowed as long
|
||||||
|
as the name is changed.
|
||||||
|
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. You just DO WHAT THE FUCK YOU WANT TO.
|
60
README.md
Normal file
60
README.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Wg Gen Web
|
||||||
|
|
||||||
|
Simple Web based configuration generator for [WireGuard](https://wireguard.com).
|
||||||
|
|
||||||
|
## Whay another one ?
|
||||||
|
|
||||||
|
All WireGuard UI implementation are trying to manage the WireGuard by applying configurations or creation network rules.
|
||||||
|
This implementation only generate configuration and its up to you to create network rules and apply configuration to WireGuard.
|
||||||
|
For example by monituring generated directory with [inotifywait](https://github.com/inotify-tools/inotify-tools/wiki).
|
||||||
|
|
||||||
|
The goal is to run Wg Gen Web in a container and WireGuard on host system.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Self-serve and web based
|
||||||
|
* Automatically select IP from networks chosen for client
|
||||||
|
* QR-Code for convenient mobile client configuration
|
||||||
|
* Enable / Disable client
|
||||||
|
* Generation of `wg0.conf` after any modification
|
||||||
|
* Dockerized
|
||||||
|
* Pretty cool look
|
||||||
|
![Screenshot](Wg-Gen-Web.png)
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
The easiest way to run wireguard-ui is using the container image
|
||||||
|
```
|
||||||
|
docker run --rm -it -v /tmp/wireguard:/data -p 8080:8080 -e "WG_CONF_DIR=/data" vx3r/wg-gen-web:latest
|
||||||
|
```
|
||||||
|
docker compose snipped
|
||||||
|
```
|
||||||
|
version: '3.6'
|
||||||
|
services:
|
||||||
|
wg-gen-web:
|
||||||
|
image: vx3r/wg-gen-web:latest
|
||||||
|
container_name: wg-gen-web
|
||||||
|
restart: unless-stopped
|
||||||
|
expose:
|
||||||
|
- "8080/tcp"
|
||||||
|
environment:
|
||||||
|
- WG_CONF_DIR=/data
|
||||||
|
- WG_INTERFACE_NAME=wg0.conf
|
||||||
|
volumes:
|
||||||
|
- /mnt/raid-lv-data/docker-persistent-data/wg-gen-web:/data
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is out of scope
|
||||||
|
|
||||||
|
* Generation or application of any `iptables` or `nftables` rules
|
||||||
|
* Application of configuration to WireGuard
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
* Full setup example with `inotifywait` and `systemd`
|
||||||
|
* Multi-user support behind [Authelia](https://github.com/authelia/authelia) (suggestions / thoughts are welcome)
|
||||||
|
* Send configs by email to client
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
* Do What the Fuck You Want to Public License. [LICENSE-WTFPL](LICENSE-WTFPL) or http://www.wtfpl.net
|
BIN
Wg-Gen-Web.png
Normal file
BIN
Wg-Gen-Web.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 150 KiB |
188
api/api.go
Normal file
188
api/api.go
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
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/model"
|
||||||
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/repository"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApplyRoutes applies router to gin Router
|
||||||
|
func ApplyRoutes(r *gin.Engine) {
|
||||||
|
client := r.Group("/api/v1.0/client")
|
||||||
|
{
|
||||||
|
|
||||||
|
client.POST("", createClient)
|
||||||
|
client.GET("/:id", readClient)
|
||||||
|
client.PATCH("/:id", updateClient)
|
||||||
|
client.DELETE("/:id", deleteClient)
|
||||||
|
client.GET("", readClients)
|
||||||
|
client.GET("/:id/config", configClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := r.Group("/api/v1.0/server")
|
||||||
|
{
|
||||||
|
server.GET("", readServer)
|
||||||
|
server.PATCH("", updateServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func createClient(c *gin.Context) {
|
||||||
|
var data model.Client
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to bind")
|
||||||
|
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := repository.CreateClient(&data)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to create client")
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readClient(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
client, err := repository.ReadClient(id)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to read client")
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateClient(c *gin.Context) {
|
||||||
|
var data model.Client
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to bind")
|
||||||
|
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := repository.UpdateClient(id, &data)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to update client")
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteClient(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
err := repository.DeleteClient(id)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to remove client")
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func readClients(c *gin.Context) {
|
||||||
|
clients, err := repository.ReadClients()
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to list clients")
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, clients)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configClient(c *gin.Context) {
|
||||||
|
configData, err := repository.ReadClientConfig(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to read client config")
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
formatQr := c.DefaultQuery("qrcode", "false")
|
||||||
|
if formatQr == "false" {
|
||||||
|
// return config as txt file
|
||||||
|
c.Header("Content-Disposition", "attachment; filename=wg0.conf")
|
||||||
|
c.Data(http.StatusOK, "application/config", configData)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// return config as png qrcode
|
||||||
|
png, err := qrcode.Encode(string(configData), qrcode.Medium, 220)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to create qrcode")
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Data(http.StatusOK, "image/png", png)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readServer(c *gin.Context) {
|
||||||
|
client, err := repository.ReadServer()
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to read client")
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateServer(c *gin.Context) {
|
||||||
|
var data model.Server
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to bind")
|
||||||
|
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := repository.UpdateServer(&data)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to update client")
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, client)
|
||||||
|
}
|
16
go.mod
Normal file
16
go.mod
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module gitlab.127-0-0-1.fr/vx3r/wg-gen-web
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e
|
||||||
|
github.com/gin-contrib/cors v1.3.0
|
||||||
|
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
|
||||||
|
github.com/gin-gonic/gin v1.5.0
|
||||||
|
github.com/joho/godotenv v1.3.0
|
||||||
|
github.com/satori/go.uuid v1.2.0
|
||||||
|
github.com/sirupsen/logrus v1.4.2
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086
|
||||||
|
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad
|
||||||
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200114203027-fcfc50b29cbb
|
||||||
|
)
|
113
go.sum
Normal file
113
go.sum
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e h1:5jVSh2l/ho6ajWhSPNN84eHEdq3dp0T7+f6r3Tc6hsk=
|
||||||
|
github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e/go.mod h1:IJgIiGUARc4aOr4bOQ85klmjsShkEEfiRc6q/yBSfo8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||||
|
github.com/gin-contrib/cors v1.3.0 h1:PolezCc89peu+NgkIWt9OB01Kbzt6IP0J/JvkG6xxlg=
|
||||||
|
github.com/gin-contrib/cors v1.3.0/go.mod h1:artPvLlhkF7oG06nK8v3U8TNz6IeX+w1uzCSEId5/Vc=
|
||||||
|
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 h1:xLG16iua01X7Gzms9045s2Y2niNpvSY/Zb1oBwgNYZY=
|
||||||
|
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
|
||||||
|
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||||
|
github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc=
|
||||||
|
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||||
|
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
|
||||||
|
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||||
|
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
|
||||||
|
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||||
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
|
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||||
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
|
||||||
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
|
||||||
|
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||||
|
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
|
||||||
|
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
|
||||||
|
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
||||||
|
github.com/mdlayher/netlink v1.0.0 h1:vySPY5Oxnn/8lxAPn2cK6kAzcZzYJl3KriSLO46OT18=
|
||||||
|
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
||||||
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
|
||||||
|
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191218084908-4a24b4065292 h1:Y8q0zsdcgAd+JU8VUA8p8Qv2YhuY9zevDG2ORt5qBUI=
|
||||||
|
golang.org/x/sys v0.0.0-20191218084908-4a24b4065292/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.zx2c4.com/wireguard v0.0.20191012 h1:sdX+y3hrHkW8KJkjY7ZgzpT5Tqo8XnBkH55U1klphko=
|
||||||
|
golang.zx2c4.com/wireguard v0.0.20191012/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
|
||||||
|
golang.zx2c4.com/wireguard v0.0.20200121 h1:vcswa5Q6f+sylDfjqyrVNNrjsFUUbPsgAQTBCAg/Qf8=
|
||||||
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200114203027-fcfc50b29cbb h1:EZFZIHfDUPApqlA2wgF5LBAXKIKAxNckrehUTPYYAHc=
|
||||||
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200114203027-fcfc50b29cbb/go.mod h1:vpFXH8L2bfaEJ/8I7DZ0CVOHsVydo6KeW9Iqh3qMa4g=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||||
|
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||||
|
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
|
||||||
|
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
79
main.go
Normal file
79
main.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/danielkov/gin-helmet"
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
|
"github.com/gin-contrib/static"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api"
|
||||||
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFormatter(&log.TextFormatter{})
|
||||||
|
log.SetOutput(os.Stderr)
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// load .env environment variables
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Fatal("failed to initialize env")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check directories or create it
|
||||||
|
if !util.DirectoryExists(filepath.Join(os.Getenv("WG_CONF_DIR"))) {
|
||||||
|
err = os.Mkdir(filepath.Join(os.Getenv("WG_CONF_DIR")), 0755)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
"dir": filepath.Join(os.Getenv("WG_CONF_DIR")),
|
||||||
|
}).Fatal("failed to mkdir")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("GIN_MODE") == "release" {
|
||||||
|
// set gin release mode
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
// disable console color
|
||||||
|
gin.DisableConsoleColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a gin router with default middleware: logger and recovery (crash-free) middleware
|
||||||
|
app := gin.Default()
|
||||||
|
|
||||||
|
// same as
|
||||||
|
config := cors.DefaultConfig()
|
||||||
|
config.AllowAllOrigins = true
|
||||||
|
app.Use(cors.New(config))
|
||||||
|
//app.Use(cors.Default())
|
||||||
|
|
||||||
|
// protection
|
||||||
|
app.Use(helmet.Default())
|
||||||
|
|
||||||
|
// no route redirect to frontend app
|
||||||
|
app.NoRoute(func(c *gin.Context) {
|
||||||
|
c.Redirect(301, "/index.html")
|
||||||
|
})
|
||||||
|
|
||||||
|
// serve static files
|
||||||
|
app.Use(static.Serve("/", static.LocalFile("./ui/dist", false)))
|
||||||
|
|
||||||
|
// apply api router
|
||||||
|
api.ApplyRoutes(app)
|
||||||
|
|
||||||
|
err = app.Run(fmt.Sprintf("%s:%s", os.Getenv("SERVER"), os.Getenv("PORT")))
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Fatal("failed to start server")
|
||||||
|
}
|
||||||
|
}
|
16
model/client.go
Normal file
16
model/client.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
AllowedIPs string `json:"allowedIPs"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
PrivateKey string `json:"privateKey"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
}
|
17
model/server.go
Normal file
17
model/server.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
ListenPort int `json:"listenPort"`
|
||||||
|
PrivateKey string `json:"privateKey"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
PresharedKey string `json:"presharedKey"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
PersistentKeepalive int `json:"persistentKeepalive"`
|
||||||
|
Dns string `json:"dns"`
|
||||||
|
}
|
335
repository/repository.go
Normal file
335
repository/repository.go
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
uuid "github.com/satori/go.uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||||
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateClient(client *model.Client) (*model.Client, error) {
|
||||||
|
u := uuid.NewV4()
|
||||||
|
client.Id = u.String()
|
||||||
|
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
client.PrivateKey = key.String()
|
||||||
|
client.PublicKey = key.PublicKey().String()
|
||||||
|
|
||||||
|
// find available IP address from selected networks
|
||||||
|
clients, err := ReadClients()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reserverIps := make([]string, 0)
|
||||||
|
for _, client := range clients {
|
||||||
|
ips := strings.Split(client.Address, ",")
|
||||||
|
for i := range ips {
|
||||||
|
if util.IsIPv6(ips[i]){
|
||||||
|
ips[i] = strings.ReplaceAll(strings.TrimSpace(ips[i]), "/128","")
|
||||||
|
} else {
|
||||||
|
ips[i] = strings.ReplaceAll(strings.TrimSpace(ips[i]), "/32","")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reserverIps = append(reserverIps, ips...)
|
||||||
|
}
|
||||||
|
|
||||||
|
networks := strings.Split(client.Address, ",")
|
||||||
|
for i := range networks {
|
||||||
|
networks[i] = strings.TrimSpace(networks[i])
|
||||||
|
}
|
||||||
|
ips := make([]string, 0)
|
||||||
|
for _, network := range networks {
|
||||||
|
ip, err := util.GetAvailableIp(network, reserverIps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if util.IsIPv6(ip){
|
||||||
|
ip = ip + "/128"
|
||||||
|
} else {
|
||||||
|
ip = ip + "/32"
|
||||||
|
}
|
||||||
|
ips = append(ips, ip)
|
||||||
|
}
|
||||||
|
client.Address = strings.Join(ips, ",")
|
||||||
|
|
||||||
|
client.Created = time.Now().UTC()
|
||||||
|
client.Updated = client.Created
|
||||||
|
|
||||||
|
err = serialize(client.Id, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := deserialize(client.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
client = v.(*model.Client)
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadClient(id string) (*model.Client, error) {
|
||||||
|
v, err := deserialize(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
client := v.(*model.Client)
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadClientConfig(id string) ([]byte, error) {
|
||||||
|
client, err := ReadClient(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := ReadServer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configDataWg, err := util.DumpClient(client, server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return configDataWg.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return server 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Update server, 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.Marshal(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
|
||||||
|
}
|
2
ui/.browserslistrc
Normal file
2
ui/.browserslistrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
19
ui/README.md
Normal file
19
ui/README.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# ui
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
9622
ui/package-lock.json
generated
Normal file
9622
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
ui/package.json
Normal file
27
ui/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.19.2",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"vue": "^2.6.10",
|
||||||
|
"vue-moment": "^4.1.0",
|
||||||
|
"vue-plugin-axios": "^1.0.14",
|
||||||
|
"vue-router": "^3.1.3",
|
||||||
|
"vuetify": "^2.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-router": "^4.1.0",
|
||||||
|
"@vue/cli-service": "^4.1.0",
|
||||||
|
"sass": "^1.19.0",
|
||||||
|
"sass-loader": "^8.0.0",
|
||||||
|
"vue-cli-plugin-vuetify": "^2.0.3",
|
||||||
|
"vue-template-compiler": "^2.6.10",
|
||||||
|
"vuetify-loader": "^1.3.0"
|
||||||
|
}
|
||||||
|
}
|
BIN
ui/public/favicon.ico
Normal file
BIN
ui/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
19
ui/public/index.html
Normal file
19
ui/public/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title>ui</title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but ui doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
33
ui/src/App.vue
Normal file
33
ui/src/App.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<v-app id="inspire">
|
||||||
|
|
||||||
|
<v-app-bar app>
|
||||||
|
<v-toolbar-title>Wg Gen Web</v-toolbar-title>
|
||||||
|
</v-app-bar>
|
||||||
|
|
||||||
|
<v-content>
|
||||||
|
<v-container>
|
||||||
|
<router-view />
|
||||||
|
</v-container>
|
||||||
|
</v-content>
|
||||||
|
|
||||||
|
<v-footer app>
|
||||||
|
<span>Copyright <a class="pr-1 pl-1" href="http://www.wtfpl.net/" target="_blank">WTFPL</a> © {{ new Date().getFullYear() }} Created with</span><v-icon class="pr-1 pl-1">mdi-heart</v-icon><span>by</span><a class="pr-1 pl-1" href="mailto:vx3r@127-0-0-1.fr">vx3r</a>
|
||||||
|
</v-footer>
|
||||||
|
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
//
|
||||||
|
}),
|
||||||
|
|
||||||
|
created () {
|
||||||
|
this.$vuetify.theme.dark = true
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
BIN
ui/src/assets/logo.png
Normal file
BIN
ui/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
1
ui/src/assets/logo.svg
Normal file
1
ui/src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>
|
After Width: | Height: | Size: 539 B |
14
ui/src/main.js
Normal file
14
ui/src/main.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import vuetify from './plugins/vuetify';
|
||||||
|
import './plugins/axios';
|
||||||
|
import './plugins/moment';
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
router,
|
||||||
|
vuetify,
|
||||||
|
render: function (h) { return h(App) }
|
||||||
|
}).$mount('#app')
|
13
ui/src/plugins/axios.js
Normal file
13
ui/src/plugins/axios.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import VueAxios from 'vue-plugin-axios'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export const myVar = 'This is my variable'
|
||||||
|
|
||||||
|
// https://www.npmjs.com/package/vue-cli-plugin-vuetify
|
||||||
|
Vue.use(VueAxios, {
|
||||||
|
axios,
|
||||||
|
config: {
|
||||||
|
baseURL: process.env.VUE_APP_API_BASE_URL || '/api/v1.0',
|
||||||
|
},
|
||||||
|
});
|
15
ui/src/plugins/moment.js
Normal file
15
ui/src/plugins/moment.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import moment from 'moment';
|
||||||
|
import VueMoment from 'vue-moment'
|
||||||
|
|
||||||
|
moment.locale('es');
|
||||||
|
|
||||||
|
Vue.use(VueMoment, {
|
||||||
|
moment
|
||||||
|
});
|
||||||
|
// $moment() accessible in project
|
||||||
|
|
||||||
|
Vue.filter('formatDate', function (value) {
|
||||||
|
if (!value) return '';
|
||||||
|
return moment(String(value)).format('YYYY-MM-DD HH:mm')
|
||||||
|
});
|
7
ui/src/plugins/vuetify.js
Normal file
7
ui/src/plugins/vuetify.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import Vuetify from 'vuetify/lib';
|
||||||
|
|
||||||
|
Vue.use(Vuetify);
|
||||||
|
|
||||||
|
export default new Vuetify({
|
||||||
|
});
|
21
ui/src/router/index.js
Normal file
21
ui/src/router/index.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import VueRouter from 'vue-router'
|
||||||
|
import Home from '../views/Home.vue'
|
||||||
|
|
||||||
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: Home
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
mode: 'history',
|
||||||
|
base: process.env.BASE_URL,
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
434
ui/src/views/Home.vue
Normal file
434
ui/src/views/Home.vue
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
<template>
|
||||||
|
<v-content>
|
||||||
|
<v-row v-if="server">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card dark>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title class="headline">Server configurations</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
<div class="d-flex flex-no-wrap justify-space-between">
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="server.name"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'Name is required',
|
||||||
|
]"
|
||||||
|
label="Friendly server name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
type="number"
|
||||||
|
v-model="server.persistentKeepalive"
|
||||||
|
label="Persistent keepalive for clients"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'Persistent keepalive is required',
|
||||||
|
]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="server.endpoint"
|
||||||
|
label="Endpoint for clients to connect to"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'Endpoint is required',
|
||||||
|
]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="server.address"
|
||||||
|
label="Server interface addresses"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'Server interface address is required',
|
||||||
|
]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="server.publicKey"
|
||||||
|
label="Server public key"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="server.presharedKey"
|
||||||
|
label="Preshared Key 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"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'Listen port is required',
|
||||||
|
]"
|
||||||
|
label="Server listen port"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer/>
|
||||||
|
<v-btn
|
||||||
|
color="warning"
|
||||||
|
@click="updateServer"
|
||||||
|
>
|
||||||
|
Update server configuration
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider dark/>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card dark>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title class="headline">Clients</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-btn
|
||||||
|
color="success"
|
||||||
|
@click.stop="dialogAddClient = true"
|
||||||
|
>
|
||||||
|
Add new client
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
v-for="(client, i) in clients"
|
||||||
|
:key="i"
|
||||||
|
cols="6"
|
||||||
|
>
|
||||||
|
<v-card
|
||||||
|
color="#1F7087"
|
||||||
|
class="mx-auto"
|
||||||
|
raised
|
||||||
|
shaped
|
||||||
|
>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title class="headline">{{ client.name }}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ client.email }}</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>Created: {{ client.created | formatDate }}</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>Updated: {{ client.updated | formatDate }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
|
||||||
|
<v-list-item-avatar
|
||||||
|
tile
|
||||||
|
size="150"
|
||||||
|
>
|
||||||
|
<v-img :src="getUrlToConfig(client.id, true)"/>
|
||||||
|
</v-list-item-avatar>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-card-text class="text--primary">
|
||||||
|
<v-chip
|
||||||
|
v-for="(ip, i) in client.address.split(',')"
|
||||||
|
:key="i"
|
||||||
|
color="indigo"
|
||||||
|
text-color="white"
|
||||||
|
>
|
||||||
|
<v-icon left>mdi-ip-network</v-icon>
|
||||||
|
{{ ip }}
|
||||||
|
</v-chip>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
:href="getUrlToConfig(client.id, false)"
|
||||||
|
>
|
||||||
|
Download configuration
|
||||||
|
<v-icon right dark>mdi-cloud-download</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
@click.stop="dialogEditClient = true; clientToEdit = client"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
<v-icon right dark>mdi-square-edit-outline</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
@click="deleteClient(client.id)"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
<v-icon right dark>mdi-trash-can-outline</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer/>
|
||||||
|
<v-tooltip right>
|
||||||
|
<template v-slot:activator="{ on }">
|
||||||
|
<v-switch
|
||||||
|
dark
|
||||||
|
v-on="on"
|
||||||
|
color="success"
|
||||||
|
v-model="client.enable"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<span>Enable or disable this client</span>
|
||||||
|
</v-tooltip>
|
||||||
|
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-dialog
|
||||||
|
v-model="dialogAddClient"
|
||||||
|
max-width="550"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">Add new client</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
v-model="valid"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="client.name"
|
||||||
|
label="Client friendly name"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'Client name is required',
|
||||||
|
]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="client.email"
|
||||||
|
label="Client email"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'E-mail is required',
|
||||||
|
v => /.+@.+\..+/.test(v) || 'E-mail must be valid',
|
||||||
|
]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="clientAddress"
|
||||||
|
:items="serverAddress"
|
||||||
|
label="Client IP will be chosen from these networks"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'Network is required',
|
||||||
|
]"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
persistent-hint
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-switch
|
||||||
|
v-model="client.enable"
|
||||||
|
color="red"
|
||||||
|
inset
|
||||||
|
:label="client.enable ? 'Enable client after creation': 'Disable client after creation'"
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer/>
|
||||||
|
<v-btn
|
||||||
|
:disabled="!valid"
|
||||||
|
color="success"
|
||||||
|
@click="addClient()"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="dialogAddClient = false"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
<v-dialog
|
||||||
|
v-if="clientToEdit"
|
||||||
|
v-model="dialogEditClient"
|
||||||
|
max-width="550"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">Edit client</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
v-model="valid"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="clientToEdit.name"
|
||||||
|
label="Client friendly name"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'Client name is required',
|
||||||
|
]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="clientToEdit.email"
|
||||||
|
label="Client email"
|
||||||
|
:rules="[
|
||||||
|
v => !!v || 'E-mail is required',
|
||||||
|
v => /.+@.+\..+/.test(v) || 'E-mail must be valid',
|
||||||
|
]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer/>
|
||||||
|
<v-btn
|
||||||
|
:disabled="!valid"
|
||||||
|
color="success"
|
||||||
|
@click="updateClient()"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="dialogEditClient = false"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
<v-snackbar
|
||||||
|
v-model="notification.show"
|
||||||
|
:right="true"
|
||||||
|
:top="true"
|
||||||
|
:color="notification.color"
|
||||||
|
>
|
||||||
|
{{ notification.text }}
|
||||||
|
<v-btn
|
||||||
|
dark
|
||||||
|
text
|
||||||
|
@click="notification.show = false"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</v-btn>
|
||||||
|
</v-snackbar>
|
||||||
|
</v-content>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'home',
|
||||||
|
mounted () {
|
||||||
|
this.getData()
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
notification: {
|
||||||
|
show: false,
|
||||||
|
color: '',
|
||||||
|
text: '',
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
checkbox: false,
|
||||||
|
server: null,
|
||||||
|
clients: [],
|
||||||
|
ipDns: "",
|
||||||
|
ipAddress: "",
|
||||||
|
clientAddress: [],
|
||||||
|
serverAddress: [],
|
||||||
|
dialogAddClient: false,
|
||||||
|
dialogEditClient: false,
|
||||||
|
clientToEdit: null,
|
||||||
|
client: {
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
enable: true,
|
||||||
|
allowedIPs: "0.0.0.0/0,::/0",
|
||||||
|
address: "",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getData() {
|
||||||
|
this.$get('/server').then((res) => {
|
||||||
|
this.server = res;
|
||||||
|
this.clientAddress = this.serverAddress = this.server.address.split(',')
|
||||||
|
}).catch((e) => {
|
||||||
|
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$get('/client').then((res) => {
|
||||||
|
this.clients = res
|
||||||
|
}).catch((e) => {
|
||||||
|
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateServer () {
|
||||||
|
this.$patch('/server', this.server).then((res) => {
|
||||||
|
this.notify('success', "Server successfully updated");
|
||||||
|
this.getData()
|
||||||
|
}).catch((e) => {
|
||||||
|
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addClient () {
|
||||||
|
this.dialogAddClient = false;
|
||||||
|
this.client.address = this.clientAddress.join(',');
|
||||||
|
this.$post('/client', this.client).then((res) => {
|
||||||
|
this.notify('success', "Client successfully added");
|
||||||
|
this.getData()
|
||||||
|
}).catch((e) => {
|
||||||
|
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteClient(id) {
|
||||||
|
if(confirm("Do you really want to delete?")){
|
||||||
|
this.$delete(`/client/${id}`).then((res) => {
|
||||||
|
this.notify('success', "Client successfully deleted");
|
||||||
|
this.getData()
|
||||||
|
}).catch((e) => {
|
||||||
|
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getUrlToConfig(id, qrcode){
|
||||||
|
let base = "/api/v1.0";
|
||||||
|
if (process.env.NODE_ENV === "development"){
|
||||||
|
base = process.env.VUE_APP_API_BASE_URL
|
||||||
|
}
|
||||||
|
if (qrcode){
|
||||||
|
return `${base}/client/${id}/config?qrcode=true`
|
||||||
|
} else {
|
||||||
|
return `${base}/client/${id}/config`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateClient() {
|
||||||
|
this.dialogEditClient = false;
|
||||||
|
this.$patch(`/client/${this.clientToEdit.id}`, this.clientToEdit).then((res) => {
|
||||||
|
this.notify('success', "Client successfully updated");
|
||||||
|
this.getData()
|
||||||
|
}).catch((e) => {
|
||||||
|
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
notify(color, msg) {
|
||||||
|
this.notification.show = true;
|
||||||
|
this.notification.color = color;
|
||||||
|
this.notification.text = msg;
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
9
ui/vue.config.js
Normal file
9
ui/vue.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
devServer: {
|
||||||
|
port: 8081,
|
||||||
|
disableHostCheck: true,
|
||||||
|
},
|
||||||
|
"transpileDependencies": [
|
||||||
|
"vuetify"
|
||||||
|
]
|
||||||
|
};
|
88
util/tpl.go
Normal file
88
util/tpl.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
clientTpl = `
|
||||||
|
[Interface]
|
||||||
|
Address = {{.Client.Address}}
|
||||||
|
PrivateKey = {{.Client.PrivateKey}}
|
||||||
|
DNS = {{.Server.Dns}}
|
||||||
|
[Peer]
|
||||||
|
PublicKey = {{.Server.PublicKey}}
|
||||||
|
PresharedKey = {{.Server.PresharedKey}}
|
||||||
|
AllowedIPs = {{.Client.AllowedIPs}}
|
||||||
|
Endpoint = {{.Server.Endpoint}}
|
||||||
|
PersistentKeepalive = {{.Server.PersistentKeepalive}}`
|
||||||
|
|
||||||
|
wgTpl = `
|
||||||
|
# {{.Server.Name}} / Updated: {{.Server.Updated}} / Created: {{.Server.Created}}
|
||||||
|
[Interface]
|
||||||
|
{{range .ServerAdresses}}
|
||||||
|
Address = {{.}}
|
||||||
|
{{end}}
|
||||||
|
ListenPort = {{.Server.ListenPort}}
|
||||||
|
PrivateKey = {{.Server.PrivateKey}}
|
||||||
|
{{$server := .Server}}
|
||||||
|
{{range .Clients}}
|
||||||
|
{{if .Enable}}
|
||||||
|
# {{.Name}} / {{.Email}} / Updated: {{.Updated}} / Created: {{.Created}}
|
||||||
|
[Peer]
|
||||||
|
PublicKey = {{.PublicKey}}
|
||||||
|
PresharedKey = {{$server.PresharedKey}}
|
||||||
|
AllowedIPs = {{.Address}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}`
|
||||||
|
)
|
||||||
|
|
||||||
|
func DumpClient(client *model.Client, server *model.Server) (bytes.Buffer, error) {
|
||||||
|
var tplBuff bytes.Buffer
|
||||||
|
|
||||||
|
t, err := template.New("client").Parse(clientTpl)
|
||||||
|
if err != nil {
|
||||||
|
return tplBuff, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dump(t, struct {
|
||||||
|
Client *model.Client
|
||||||
|
Server *model.Server
|
||||||
|
}{
|
||||||
|
Client: client,
|
||||||
|
Server: server,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DumpServerWg(clients []*model.Client, server *model.Server) (bytes.Buffer, error) {
|
||||||
|
var tplBuff bytes.Buffer
|
||||||
|
|
||||||
|
t, err := template.New("server").Parse(wgTpl)
|
||||||
|
if err != nil {
|
||||||
|
return tplBuff, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dump(t, struct {
|
||||||
|
Clients []*model.Client
|
||||||
|
Server *model.Server
|
||||||
|
ServerAdresses []string
|
||||||
|
}{
|
||||||
|
ServerAdresses: strings.Split(server.Address, ","),
|
||||||
|
Clients: clients,
|
||||||
|
Server: server,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func dump(tpl *template.Template , data interface{}) (bytes.Buffer, error) {
|
||||||
|
var tplBuff bytes.Buffer
|
||||||
|
|
||||||
|
err := tpl.Execute(&tplBuff, data)
|
||||||
|
if err != nil {
|
||||||
|
return tplBuff, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tplBuff, nil
|
||||||
|
}
|
93
util/util.go
Normal file
93
util/util.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReadFile(path string) (bytes []byte, err error) {
|
||||||
|
bytes, err = ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteFile(path string, bytes []byte) (err error) {
|
||||||
|
err = ioutil.WriteFile(path, bytes, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileExists(name string) bool {
|
||||||
|
info, err := os.Stat(name)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DirectoryExists(name string) bool {
|
||||||
|
info, err := os.Stat(name)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAvailableIp(cidr string, reserved []string) (string, error) {
|
||||||
|
addresses, err := GetAllAddressesFromCidr(cidr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addresse := range addresses {
|
||||||
|
ok := true
|
||||||
|
for _, r := range reserved {
|
||||||
|
if addresse == r {
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return addresse, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("no more available address from cidr")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllAddressesFromCidr(cidr string) ([]string, error) {
|
||||||
|
ip, ipnet, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ips []string
|
||||||
|
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
|
||||||
|
ips = append(ips, ip.String())
|
||||||
|
}
|
||||||
|
// remove network address and broadcast address (and server ip .1)
|
||||||
|
return ips[2 : len(ips)-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://play.golang.org/p/m8TNTtygK0
|
||||||
|
func inc(ip net.IP) {
|
||||||
|
for j := len(ip) - 1; j >= 0; j-- {
|
||||||
|
ip[j]++
|
||||||
|
if ip[j] > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsIPv6(address string) bool {
|
||||||
|
return strings.Count(address, ":") >= 2
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user