first commit
This commit is contained in:
commit
364dfbbe07
12
.env
Normal file
12
.env
Normal file
@ -0,0 +1,12 @@
|
||||
SERVER=0.0.0.0
|
||||
PORT=8080
|
||||
GIN_MODE=release
|
||||
|
||||
WG_CONF_DIR=./wireguard
|
||||
WG_INTERFACE_NAME=wg0.conf
|
||||
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=account@gmail.com
|
||||
SMTP_PASSWORD="*************"
|
||||
SMTP_FROM="Wg Gen Web <account@gmail.com>"
|
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@ -0,0 +1,25 @@
|
||||
ARG COMMIT="N/A"
|
||||
|
||||
FROM golang:alpine AS build-back
|
||||
WORKDIR /app
|
||||
ARG COMMIT
|
||||
COPY . .
|
||||
RUN go build -ldflags="-X 'gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util.Version=${COMMIT}'" -o wg-gen-web-linux
|
||||
|
||||
FROM node:10-alpine AS build-front
|
||||
WORKDIR /app
|
||||
COPY ui/package*.json ./
|
||||
RUN npm install
|
||||
COPY ui/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM alpine
|
||||
WORKDIR /app
|
||||
COPY --from=build-back /app/wg-gen-web-linux .
|
||||
COPY --from=build-front /app/dist ./ui/dist
|
||||
COPY .env .
|
||||
RUN chmod +x ./wg-gen-web-linux
|
||||
RUN apk add --no-cache ca-certificates
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["/app/wg-gen-web-linux"]
|
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.
|
241
LISEZMOI.md
Normal file
241
LISEZMOI.md
Normal file
@ -0,0 +1,241 @@
|
||||
# Wg Gen Web
|
||||
|
||||
<h1><img src="./web-ui.png" ></h1>
|
||||
|
||||
Générateur de configuration simple basé sur le Web pour [WireGuard](https://wireguard.com).
|
||||
|
||||
## Why another one ?
|
||||
|
||||
Toutes les implémentations de WireGuard UI essaient de gérer le service en appliquant des configurations et en créant des règles de réseau.
|
||||
|
||||
Cette implémentation ne fait que générer de la configuration et c'est à vous de créer des règles réseau et d'appliquer la configuration à WireGuard.
|
||||
|
||||
Par exemple en surveillant le répertoire généré avec [inotifywait](https://github.com/inotify-tools/inotify-tools/wiki) ou avec systemd.path.
|
||||
|
||||
Le but est d'exécuter Wg-Gen-Web dans un espace et WireGuard sur le système hôte.
|
||||
|
||||
### Caractéristiques
|
||||
|
||||
* Libre-service et basé sur le web
|
||||
* QR-Code pour une configuration pratique du client mobile
|
||||
* Support optionnel multi-utilisateurs derrière un proxy d'authentification
|
||||
* Prise en charge de l'authentification simple
|
||||
* Zéro dépendance externe - juste un binaire unique utilisant le module noyau wireguard
|
||||
* Déploiement de binaires et de conteneurs
|
||||
|
||||
Vous devez avoir WireGuard installé sur la machine qui exécute `wg-ui`.
|
||||
|
||||
## Wg ui
|
||||
|
||||
### Installation de Go (Debian)
|
||||
|
||||
Installer la dernière version de Go à partir de (<https://golang.org/dl/>)
|
||||
|
||||
```
|
||||
cd ~
|
||||
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
|
||||
sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
|
||||
echo "export PATH=$PATH:/usr/local/go/bin" >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
Installer la version LTS de nodejs pour le frontend
|
||||
|
||||
```
|
||||
# debian
|
||||
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo bash -
|
||||
sudo apt update
|
||||
sudo apt install nodejs
|
||||
```
|
||||
|
||||
### Cloner wg-webui-fr
|
||||
|
||||
```
|
||||
git clone https://gitea.rnmkcy.eu/yann/wg-webui-fr
|
||||
```
|
||||
|
||||
### Construire wg-webui-fr
|
||||
|
||||
Exécuter les commandes suivantes
|
||||
|
||||
```
|
||||
cd ~/wg-webui-fr/
|
||||
go mod tidy
|
||||
go build -o wg-ui main.go
|
||||
cd ui
|
||||
export NODE_OPTIONS=--openssl-legacy-provider
|
||||
export VUE_APP_API_BASE_URL=http://localhost:8080/api/v1.0
|
||||
npm install
|
||||
npm run build
|
||||
sudo mkdir -p /opt/appwg/ui
|
||||
sudo cp ../wg-ui /opt/appwg
|
||||
sudo cp -r dist /opt/appwg/ui/
|
||||
```
|
||||
|
||||
### Environnement
|
||||
|
||||
Créer un fichier environnement `/opt/appwg/.env`
|
||||
|
||||
```
|
||||
SERVER=127.0.0.1
|
||||
PORT=8090
|
||||
GIN_MODE=debug
|
||||
|
||||
WG_CONF_DIR=/opt/appwg/wireguard
|
||||
WG_INTERFACE_NAME=wg0.conf
|
||||
|
||||
SMTP_HOST="mx.exemple.com"
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME="Utilisateur@exemple.com"
|
||||
SMTP_PASSWORD="Mot de passe"
|
||||
SMTP_FROM="Utilisateur@exemple.com"
|
||||
```
|
||||
|
||||
ATTENTION!!! il faut au minimun un fichier `wg0.conf` dans `/opt/appwg/wireguard`
|
||||
Avec une installation de l'application wireguard, `WG_CONF_DIR=/etc/wireguard`
|
||||
|
||||
### Utilisation systemd
|
||||
|
||||
Créer le service `/etc/systemd/system/wgweb.service`
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Wireguard web
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
|
||||
Type=simple
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
WorkingDirectory=/opt/appwg
|
||||
ExecStart=/opt/appwg/wg-ui
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Recharger
|
||||
|
||||
```
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
### Activer et lancer le service
|
||||
|
||||
```
|
||||
sudo systemctl enable wgweb.service --now
|
||||
```
|
||||
|
||||
Wireguard ui est accessible sur localhost:8090
|
||||
|
||||
Pour un accès via ssh
|
||||
|
||||
```
|
||||
ssh -L 9500:localhost:8090 utilisateur@adresse_serveur
|
||||
```
|
||||
|
||||
### Accès navigateur
|
||||
|
||||
Ensuite ouvrir un navigateur localement sur localhost:9500
|
||||
|
||||
![](web-ui-a.png)
|
||||
|
||||
Activer le service
|
||||
|
||||
sudo systemctl enable wgweb.service
|
||||
|
||||
### Mise è jour wg-webui-fr
|
||||
|
||||
Exécuter les commandes suivantes
|
||||
|
||||
```bash
|
||||
echo "Arrêt wgweb.service"
|
||||
sudo systemctl stop wgweb.service
|
||||
echo "Sauvegarde"
|
||||
sudo cp /opt/appwg/.env _.env
|
||||
echo "Supprimer appwg"
|
||||
sudo rm -r /opt/appwg
|
||||
echo "Construire wg-ui main.go"
|
||||
cd $HOME/wg-webui-fr/
|
||||
go mod tidy
|
||||
go build -o wg-ui main.go
|
||||
cd ui
|
||||
export NODE_OPTIONS=--openssl-legacy-provider
|
||||
export VUE_APP_API_BASE_URL=http://localhost:8080/api/v1.0
|
||||
npm install
|
||||
npm run build
|
||||
sudo mkdir -p /opt/appwg/ui
|
||||
sudo cp ../wg-ui /opt/appwg
|
||||
sudo cp -r dist /opt/appwg/ui/
|
||||
echo "Restaurer environnement"
|
||||
sudo cp $HOME/_.env /opt/appwg/.env
|
||||
echo "Démarrer le service"
|
||||
sudo systemctl start wgweb.service
|
||||
echo "FIN reconstruction"
|
||||
```
|
||||
|
||||
Wireguard ui est accessible sur localhost:8090
|
||||
|
||||
|
||||
### Appliquer automatiquement les changements à WireGuard
|
||||
|
||||
#### Utilisation de systemd (DEFAUT)
|
||||
|
||||
Utilisation de `systemd.path` pour surveiller les changements de répertoire voir [systemd doc](https://www.freedesktop.org/software/systemd/man/systemd.path.html)
|
||||
|
||||
```
|
||||
# /etc/systemd/system/wg-gen-web.path
|
||||
[Unit]
|
||||
Description=Watch /etc/wireguard for changes
|
||||
|
||||
[Path]
|
||||
PathModified=/etc/wireguard
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Ce `.path` activera le fichier unit avec le même nom
|
||||
|
||||
```
|
||||
# /etc/systemd/system/wg-gen-web.service
|
||||
[Unit]
|
||||
Description=Restart WireGuard
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/systemctl restart wg-quick@wg0.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Ce qui va redémarrer le service WireGuard
|
||||
|
||||
Activer et lancer
|
||||
|
||||
```
|
||||
sudo systemctl enable wg-gen-web.path --now
|
||||
```
|
||||
|
||||
### Utiliser inotifywait
|
||||
|
||||
Pour tout autre système d'initialisation, créez un daemon exécutant ce script
|
||||
|
||||
```
|
||||
#!/bin/sh
|
||||
while inotifywait -e modify -e create /etc/wireguard; do
|
||||
wg-quick down wg0
|
||||
wg-quick up wg0
|
||||
done
|
||||
```
|
||||
|
||||
### Comment utiliser avec une configuration WireGuard existante
|
||||
|
||||
Après le premier lancement, Wg Gen Web créera le fichier `server.json` dans le répertoire défini le paramètre `WG_INTERFACE_NAME` du fichier environnement.
|
||||
|
||||
Modifier le fichier existant **server.json** pour être identique au paramétrage de wireguard **wg0.conf**
|
245
api/api.go
Normal file
245
api/api.go
Normal file
@ -0,0 +1,245 @@
|
||||
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/core"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/template"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
||||
"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)
|
||||
client.GET("/:id/email", emailClient)
|
||||
}
|
||||
|
||||
server := r.Group("/api/v1.0/server")
|
||||
{
|
||||
server.GET("", readServer)
|
||||
server.PATCH("", updateServer)
|
||||
server.GET("/config", configServer)
|
||||
server.GET("/version", version)
|
||||
}
|
||||
}
|
||||
|
||||
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 := core.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 := core.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 := core.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 := core.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 := core.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 := core.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
|
||||
}
|
||||
// return config as png qrcode
|
||||
png, err := qrcode.Encode(string(configData), qrcode.Medium, 250)
|
||||
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 emailClient(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
err := core.EmailClient(id)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to send email to client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
func readServer(c *gin.Context) {
|
||||
client, err := core.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 := core.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)
|
||||
}
|
||||
|
||||
func configServer(c *gin.Context) {
|
||||
clients, err := core.ReadClients()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read clients")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
server, err := core.ReadServer()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read server")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := template.DumpServerWg(clients, server)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to dump wg config")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
// return config as txt file
|
||||
c.Header("Content-Disposition", "attachment; filename=wg0.conf")
|
||||
c.Data(http.StatusOK, "application/config", configData)
|
||||
}
|
||||
|
||||
func version(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"version": util.Version,
|
||||
})
|
||||
}
|
13
appwg/.env
Executable file
13
appwg/.env
Executable file
@ -0,0 +1,13 @@
|
||||
SERVER=127.0.0.1
|
||||
PORT=8100
|
||||
GIN_MODE=release
|
||||
|
||||
WG_CONF_DIR=/etc/wireguard
|
||||
WG_INTERFACE_NAME=wg0.conf
|
||||
|
||||
SMTP_HOST=xoyaz.xyz
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME="ian@xoyaz.xyz"
|
||||
SMTP_PASSWORD="HaveuseOrmeauxEncageSaisine"
|
||||
SMTP_FROM="ian@xoyaz.xyz"
|
||||
|
1
appwg/ui/dist/css/Clients.56a11097.css
vendored
Executable file
1
appwg/ui/dist/css/Clients.56a11097.css
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/css/Clients~Server.f87dfffe.css
vendored
Executable file
1
appwg/ui/dist/css/Clients~Server.f87dfffe.css
vendored
Executable file
File diff suppressed because one or more lines are too long
5
appwg/ui/dist/css/chunk-vendors.0ec741de.css
vendored
Executable file
5
appwg/ui/dist/css/chunk-vendors.0ec741de.css
vendored
Executable file
File diff suppressed because one or more lines are too long
BIN
appwg/ui/dist/favicon.png
vendored
Executable file
BIN
appwg/ui/dist/favicon.png
vendored
Executable file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
appwg/ui/dist/img/logo.8f75d612.png
vendored
Executable file
BIN
appwg/ui/dist/img/logo.8f75d612.png
vendored
Executable file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
1
appwg/ui/dist/index.html
vendored
Executable file
1
appwg/ui/dist/index.html
vendored
Executable file
@ -0,0 +1 @@
|
||||
<!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"><link rel="icon" href="/favicon.png"><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"><link href="/css/Clients.56a11097.css" rel="prefetch"><link href="/css/Clients~Server.f87dfffe.css" rel="prefetch"><link href="/js/Clients.adbe44c3.js" rel="prefetch"><link href="/js/Clients~Server.12421a31.js" rel="prefetch"><link href="/js/Index.711255c3.js" rel="prefetch"><link href="/js/Server.e0cb69bc.js" rel="prefetch"><link href="/css/chunk-vendors.0ec741de.css" rel="preload" as="style"><link href="/js/app.12cb1049.js" rel="preload" as="script"><link href="/js/chunk-vendors.40c1d4b4.js" rel="preload" as="script"><link href="/css/chunk-vendors.0ec741de.css" rel="stylesheet"></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><script src="/js/chunk-vendors.40c1d4b4.js"></script><script src="/js/app.12cb1049.js"></script></body></html>
|
2
appwg/ui/dist/js/Clients.1916468e.js
vendored
Executable file
2
appwg/ui/dist/js/Clients.1916468e.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/Clients.1916468e.js.map
vendored
Executable file
1
appwg/ui/dist/js/Clients.1916468e.js.map
vendored
Executable file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/Clients.64ae6101.js
vendored
Executable file
2
appwg/ui/dist/js/Clients.64ae6101.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/Clients.64ae6101.js.map
vendored
Executable file
1
appwg/ui/dist/js/Clients.64ae6101.js.map
vendored
Executable file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/Clients.adbe44c3.js
vendored
Normal file
2
appwg/ui/dist/js/Clients.adbe44c3.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/Clients.adbe44c3.js.map
vendored
Normal file
1
appwg/ui/dist/js/Clients.adbe44c3.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/Clients.c3e03423.js
vendored
Executable file
2
appwg/ui/dist/js/Clients.c3e03423.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/Clients.c3e03423.js.map
vendored
Executable file
1
appwg/ui/dist/js/Clients.c3e03423.js.map
vendored
Executable file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/Clients.d1e169a5.js
vendored
Executable file
2
appwg/ui/dist/js/Clients.d1e169a5.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/Clients.d1e169a5.js.map
vendored
Executable file
1
appwg/ui/dist/js/Clients.d1e169a5.js.map
vendored
Executable file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/Clients~Server.12421a31.js
vendored
Normal file
2
appwg/ui/dist/js/Clients~Server.12421a31.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/Clients~Server.12421a31.js.map
vendored
Normal file
1
appwg/ui/dist/js/Clients~Server.12421a31.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/Clients~Server.f3845244.js
vendored
Executable file
2
appwg/ui/dist/js/Clients~Server.f3845244.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/Clients~Server.f3845244.js.map
vendored
Executable file
1
appwg/ui/dist/js/Clients~Server.f3845244.js.map
vendored
Executable file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/Index.39a233d4.js
vendored
Executable file
2
appwg/ui/dist/js/Index.39a233d4.js
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["Index"],{d504:function(e,n,t){"use strict";t.r(n);var r=function(){var e=this,n=e._self._c;return n("div")},c=[],l={created(){this.$router.replace({name:"clients"})}},s=l,u=t("2877"),a=Object(u["a"])(s,r,c,!1,null,null,null);n["default"]=a.exports}}]);
|
||||
//# sourceMappingURL=Index.39a233d4.js.map
|
1
appwg/ui/dist/js/Index.39a233d4.js.map
vendored
Executable file
1
appwg/ui/dist/js/Index.39a233d4.js.map
vendored
Executable file
@ -0,0 +1 @@
|
||||
{"version":3,"sources":["webpack:///./src/views/Index.vue?e07c","webpack:///src/views/Index.vue","webpack:///./src/views/Index.vue?062b","webpack:///./src/views/Index.vue"],"names":["render","_vm","this","_c","_self","staticRenderFns","$router","replace","name","component"],"mappings":"8GAAA,IAAIA,EAAS,WAAkB,IAAIC,EAAIC,KAAKC,EAAGF,EAAIG,MAAMD,GAAG,OAAOA,EAAG,QAElEE,EAAkB,GCEL,GACb,UACEH,KAAKI,QAAQC,QAAQ,CAAEC,KAAM,cCN0J,I,YCOzLC,EAAY,eACd,EACAT,EACAK,GACA,EACA,KACA,KACA,MAIa,aAAAI,E","file":"js/Index.39a233d4.js","sourcesContent":["var render = function render(){var _vm=this,_c=_vm._self._c;return _c(\"div\")\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","<template>\n</template>\n\n<script>\n export default {\n created () {\n this.$router.replace({ name: 'clients' })\n }\n }\n</script>\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--1-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Index.vue?vue&type=script&lang=js\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--1-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Index.vue?vue&type=script&lang=js\"","import { render, staticRenderFns } from \"./Index.vue?vue&type=template&id=2e4a022b\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports"],"sourceRoot":""}
|
2
appwg/ui/dist/js/Index.711255c3.js
vendored
Normal file
2
appwg/ui/dist/js/Index.711255c3.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["Index"],{d504:function(e,n,t){"use strict";t.r(n);var r=function(){var e=this,n=e._self._c;return n("div")},c=[],l={created(){this.$router.replace({name:"clients"})}},s=l,u=t("2877"),a=Object(u["a"])(s,r,c,!1,null,null,null);n["default"]=a.exports}}]);
|
||||
//# sourceMappingURL=Index.711255c3.js.map
|
1
appwg/ui/dist/js/Index.711255c3.js.map
vendored
Normal file
1
appwg/ui/dist/js/Index.711255c3.js.map
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"sources":["webpack:///./src/views/Index.vue?2335","webpack:///src/views/Index.vue","webpack:///./src/views/Index.vue?062b","webpack:///./src/views/Index.vue"],"names":["render","_vm","this","_c","_self","staticRenderFns","$router","replace","name","component"],"mappings":"8GAAA,IAAIA,EAAS,WAAkB,IAAIC,EAAIC,KAAKC,EAAGF,EAAIG,MAAMD,GAAG,OAAOA,EAAG,QAElEE,EAAkB,GCEL,GACb,UACEH,KAAKI,QAAQC,QAAQ,CAAEC,KAAM,cCN0J,I,YCOzLC,EAAY,eACd,EACAT,EACAK,GACA,EACA,KACA,KACA,MAIa,aAAAI,E","file":"js/Index.711255c3.js","sourcesContent":["var render = function render(){var _vm=this,_c=_vm._self._c;return _c(\"div\")\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","<template>\n</template>\n\n<script>\n export default {\n created () {\n this.$router.replace({ name: 'clients' })\n }\n }\n</script>\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--1-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Index.vue?vue&type=script&lang=js\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--1-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Index.vue?vue&type=script&lang=js\"","import { render, staticRenderFns } from \"./Index.vue?vue&type=template&id=2e4a022b\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports"],"sourceRoot":""}
|
2
appwg/ui/dist/js/Index.72bdca15.js
vendored
Executable file
2
appwg/ui/dist/js/Index.72bdca15.js
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["Index"],{d504:function(e,n,t){"use strict";t.r(n);var r=function(){var e=this,n=e._self._c;return n("div")},c=[],l={created(){this.$router.replace({name:"clients"})}},s=l,u=t("2877"),a=Object(u["a"])(s,r,c,!1,null,null,null);n["default"]=a.exports}}]);
|
||||
//# sourceMappingURL=Index.72bdca15.js.map
|
1
appwg/ui/dist/js/Index.72bdca15.js.map
vendored
Executable file
1
appwg/ui/dist/js/Index.72bdca15.js.map
vendored
Executable file
@ -0,0 +1 @@
|
||||
{"version":3,"sources":["webpack:///./src/views/Index.vue?363c","webpack:///src/views/Index.vue","webpack:///./src/views/Index.vue?062b","webpack:///./src/views/Index.vue"],"names":["render","_vm","this","_c","_self","staticRenderFns","$router","replace","name","component"],"mappings":"8GAAA,IAAIA,EAAS,WAAkB,IAAIC,EAAIC,KAAKC,EAAGF,EAAIG,MAAMD,GAAG,OAAOA,EAAG,QAElEE,EAAkB,GCEL,GACb,UACEH,KAAKI,QAAQC,QAAQ,CAAEC,KAAM,cCN0J,I,YCOzLC,EAAY,eACd,EACAT,EACAK,GACA,EACA,KACA,KACA,MAIa,aAAAI,E","file":"js/Index.72bdca15.js","sourcesContent":["var render = function render(){var _vm=this,_c=_vm._self._c;return _c(\"div\")\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","<template>\n</template>\n\n<script>\n export default {\n created () {\n this.$router.replace({ name: 'clients' })\n }\n }\n</script>\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--1-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Index.vue?vue&type=script&lang=js\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--1-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Index.vue?vue&type=script&lang=js\"","import { render, staticRenderFns } from \"./Index.vue?vue&type=template&id=2e4a022b\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports"],"sourceRoot":""}
|
2
appwg/ui/dist/js/Server.2c31711e.js
vendored
Executable file
2
appwg/ui/dist/js/Server.2c31711e.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/Server.2c31711e.js.map
vendored
Executable file
1
appwg/ui/dist/js/Server.2c31711e.js.map
vendored
Executable file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/Server.33db6d96.js
vendored
Executable file
2
appwg/ui/dist/js/Server.33db6d96.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/Server.33db6d96.js.map
vendored
Executable file
1
appwg/ui/dist/js/Server.33db6d96.js.map
vendored
Executable file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/Server.e0cb69bc.js
vendored
Normal file
2
appwg/ui/dist/js/Server.e0cb69bc.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/Server.e0cb69bc.js.map
vendored
Normal file
1
appwg/ui/dist/js/Server.e0cb69bc.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/app.12cb1049.js
vendored
Normal file
2
appwg/ui/dist/js/app.12cb1049.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/app.12cb1049.js.map
vendored
Normal file
1
appwg/ui/dist/js/app.12cb1049.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/app.2e28011b.js
vendored
Executable file
2
appwg/ui/dist/js/app.2e28011b.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/app.2e28011b.js.map
vendored
Executable file
1
appwg/ui/dist/js/app.2e28011b.js.map
vendored
Executable file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/app.50b90708.js
vendored
Executable file
2
appwg/ui/dist/js/app.50b90708.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/app.50b90708.js.map
vendored
Executable file
1
appwg/ui/dist/js/app.50b90708.js.map
vendored
Executable file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/app.a3a42c5a.js
vendored
Executable file
2
appwg/ui/dist/js/app.a3a42c5a.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/app.a3a42c5a.js.map
vendored
Executable file
1
appwg/ui/dist/js/app.a3a42c5a.js.map
vendored
Executable file
File diff suppressed because one or more lines are too long
2
appwg/ui/dist/js/app.d8243852.js
vendored
Executable file
2
appwg/ui/dist/js/app.d8243852.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/app.d8243852.js.map
vendored
Executable file
1
appwg/ui/dist/js/app.d8243852.js.map
vendored
Executable file
File diff suppressed because one or more lines are too long
291
appwg/ui/dist/js/chunk-vendors.40c1d4b4.js
vendored
Normal file
291
appwg/ui/dist/js/chunk-vendors.40c1d4b4.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/chunk-vendors.40c1d4b4.js.map
vendored
Normal file
1
appwg/ui/dist/js/chunk-vendors.40c1d4b4.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
287
appwg/ui/dist/js/chunk-vendors.b9edb8dc.js
vendored
Executable file
287
appwg/ui/dist/js/chunk-vendors.b9edb8dc.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
appwg/ui/dist/js/chunk-vendors.b9edb8dc.js.map
vendored
Executable file
1
appwg/ui/dist/js/chunk-vendors.b9edb8dc.js.map
vendored
Executable file
File diff suppressed because one or more lines are too long
BIN
appwg/wg-ui
Executable file
BIN
appwg/wg-ui
Executable file
Binary file not shown.
280
core/client.go
Normal file
280
core/client.go
Normal file
@ -0,0 +1,280 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"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"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateClient client with all necessary data
|
||||
func CreateClient(client *model.Client) (*model.Client, error) {
|
||||
// check if client is valid
|
||||
errs := client.IsValid()
|
||||
if len(errs) != 0 {
|
||||
for _, err := range errs {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("client validation error")
|
||||
}
|
||||
return nil, errors.New("failed to validate client")
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
presharedKey, err := wgtypes.GenerateKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.PresharedKey = presharedKey.String()
|
||||
|
||||
reserverIps, err := GetAllReservedIps()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ips := make([]string, 0)
|
||||
for _, network := range client.Address {
|
||||
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 = ips
|
||||
client.Created = time.Now().UTC()
|
||||
client.Updated = client.Created
|
||||
|
||||
err = storage.Serialize(client.Id, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := storage.Deserialize(client.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client = v.(*model.Client)
|
||||
|
||||
// data modified, dump new config
|
||||
return client, UpdateServerConfigWg()
|
||||
}
|
||||
|
||||
// ReadClient client by id
|
||||
func ReadClient(id string) (*model.Client, error) {
|
||||
v, err := storage.Deserialize(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := v.(*model.Client)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// check if client is valid
|
||||
errs := client.IsValid()
|
||||
if len(errs) != 0 {
|
||||
for _, err := range errs {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("client validation error")
|
||||
}
|
||||
return nil, errors.New("failed to validate client")
|
||||
}
|
||||
|
||||
// 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 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
|
||||
}
|
||||
|
||||
// ReadClientConfig in wg format
|
||||
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 := template.DumpClientWg(client, server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return configDataWg, nil
|
||||
}
|
||||
|
||||
// SendEmail to client
|
||||
func EmailClient(id string) error {
|
||||
client, err := ReadClient(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configData, err := ReadClientConfig(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// conf as .conf file
|
||||
tmpfileCfg, err := ioutil.TempFile("", "wireguard-vpn-*.conf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tmpfileCfg.Write(configData); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tmpfileCfg.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpfileCfg.Name()) // clean up
|
||||
|
||||
// conf as png image
|
||||
png, err := qrcode.Encode(string(configData), qrcode.Medium, 280)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpfilePng, err := ioutil.TempFile("", "qrcode-*.png")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tmpfilePng.Write(png); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tmpfilePng.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpfilePng.Name()) // clean up
|
||||
|
||||
// get email body
|
||||
emailBody, err := template.DumpEmail(client, filepath.Base(tmpfilePng.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// port to int
|
||||
port, err := strconv.Atoi(os.Getenv("SMTP_PORT"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d := gomail.NewDialer(os.Getenv("SMTP_HOST"), port, os.Getenv("SMTP_USERNAME"), os.Getenv("SMTP_PASSWORD"))
|
||||
s, err := d.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m := gomail.NewMessage()
|
||||
|
||||
m.SetHeader("From", os.Getenv("SMTP_FROM"))
|
||||
m.SetAddressHeader("To", client.Email, client.Name)
|
||||
m.SetHeader("Subject", "WireGuard VPN Configuration")
|
||||
m.SetBody("text/html", string(emailBody))
|
||||
m.Attach(tmpfileCfg.Name())
|
||||
m.Embed(tmpfilePng.Name())
|
||||
|
||||
err = gomail.Send(s, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
309
core/migrate.go
Normal file
309
core/migrate.go
Normal file
@ -0,0 +1,309 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
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/storage"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Migrate all changes, current struct fields change
|
||||
func MigrateInitialStructChange() error {
|
||||
clients, err := readClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := deserialize("server.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, client := range clients {
|
||||
switch v := client["allowedIPs"].(type) {
|
||||
case []interface{}:
|
||||
log.Infof("client %s has been already migrated", client["id"])
|
||||
continue
|
||||
default:
|
||||
log.Infof("unexpected type %T, mus be migrated", v)
|
||||
}
|
||||
|
||||
c := &model.Client{}
|
||||
c.Id = client["id"].(string)
|
||||
c.Name = client["name"].(string)
|
||||
c.Email = client["email"].(string)
|
||||
c.Enable = client["enable"].(bool)
|
||||
c.AllowedIPs = make([]string, 0)
|
||||
for _, address := range strings.Split(client["allowedIPs"].(string), ",") {
|
||||
if util.IsValidCidr(strings.TrimSpace(address)) {
|
||||
c.AllowedIPs = append(c.AllowedIPs, strings.TrimSpace(address))
|
||||
}
|
||||
}
|
||||
c.Address = make([]string, 0)
|
||||
for _, address := range strings.Split(client["address"].(string), ",") {
|
||||
if util.IsValidCidr(strings.TrimSpace(address)) {
|
||||
c.Address = append(c.Address, strings.TrimSpace(address))
|
||||
}
|
||||
}
|
||||
c.PrivateKey = client["privateKey"].(string)
|
||||
c.PublicKey = client["publicKey"].(string)
|
||||
created, err := time.Parse(time.RFC3339, client["created"].(string))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Errorf("failed to parse time")
|
||||
continue
|
||||
}
|
||||
c.Created = created
|
||||
updated, err := time.Parse(time.RFC3339, client["updated"].(string))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Errorf("failed to parse time")
|
||||
continue
|
||||
}
|
||||
c.Updated = updated
|
||||
|
||||
err = storage.Serialize(c.Id, c)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Errorf("failed to Serialize client")
|
||||
}
|
||||
}
|
||||
|
||||
switch v := s["address"].(type) {
|
||||
case []interface{}:
|
||||
log.Info("server has been already migrated")
|
||||
return nil
|
||||
default:
|
||||
log.Infof("unexpected type %T, mus be migrated", v)
|
||||
}
|
||||
|
||||
server := &model.Server{}
|
||||
|
||||
server.Address = make([]string, 0)
|
||||
for _, address := range strings.Split(s["address"].(string), ",") {
|
||||
if util.IsValidCidr(strings.TrimSpace(address)) {
|
||||
server.Address = append(server.Address, strings.TrimSpace(address))
|
||||
}
|
||||
}
|
||||
server.ListenPort = int(s["listenPort"].(float64))
|
||||
server.PrivateKey = s["privateKey"].(string)
|
||||
server.PublicKey = s["publicKey"].(string)
|
||||
//server.PresharedKey = s["presharedKey"].(string)
|
||||
server.Endpoint = s["endpoint"].(string)
|
||||
server.PersistentKeepalive = int(s["persistentKeepalive"].(float64))
|
||||
server.Dns = make([]string, 0)
|
||||
for _, address := range strings.Split(s["dns"].(string), ",") {
|
||||
if util.IsValidIp(strings.TrimSpace(address)) {
|
||||
server.Dns = append(server.Dns, strings.TrimSpace(address))
|
||||
}
|
||||
}
|
||||
if val, ok := s["preUp"]; ok {
|
||||
server.PreUp = val.(string)
|
||||
}
|
||||
if val, ok := s["postUp"]; ok {
|
||||
server.PostUp = val.(string)
|
||||
}
|
||||
if val, ok := s["preDown"]; ok {
|
||||
server.PreDown = val.(string)
|
||||
}
|
||||
if val, ok := s["postDown"]; ok {
|
||||
server.PostDown = val.(string)
|
||||
}
|
||||
created, err := time.Parse(time.RFC3339, s["created"].(string))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Errorf("failed to parse time")
|
||||
}
|
||||
server.Created = created
|
||||
updated, err := time.Parse(time.RFC3339, s["updated"].(string))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Errorf("failed to parse time")
|
||||
}
|
||||
server.Updated = updated
|
||||
|
||||
err = storage.Serialize("server.json", server)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Errorf("failed to Serialize server")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrate presharedKey issue #23
|
||||
func MigratePresharedKey() error {
|
||||
clients, err := readClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := deserialize("server.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, client := range clients {
|
||||
if _, ok := client["presharedKey"]; ok {
|
||||
log.Infof("client %s has been already migrated for preshared key", client["id"])
|
||||
continue
|
||||
}
|
||||
|
||||
c := &model.Client{}
|
||||
c.Id = client["id"].(string)
|
||||
c.Name = client["name"].(string)
|
||||
c.Email = client["email"].(string)
|
||||
c.Enable = client["enable"].(bool)
|
||||
if val, ok := client["ignorePersistentKeepalive"]; ok {
|
||||
c.IgnorePersistentKeepalive = val.(bool)
|
||||
} else {
|
||||
c.IgnorePersistentKeepalive = false
|
||||
}
|
||||
c.PresharedKey = s["presharedKey"].(string)
|
||||
c.AllowedIPs = make([]string, 0)
|
||||
for _, address := range client["allowedIPs"].([]interface{}) {
|
||||
c.AllowedIPs = append(c.AllowedIPs, address.(string))
|
||||
}
|
||||
c.Address = make([]string, 0)
|
||||
for _, address := range client["address"].([]interface{}) {
|
||||
c.Address = append(c.Address, address.(string))
|
||||
}
|
||||
c.PrivateKey = client["privateKey"].(string)
|
||||
c.PublicKey = client["publicKey"].(string)
|
||||
created, err := time.Parse(time.RFC3339, client["created"].(string))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Errorf("failed to parse time")
|
||||
continue
|
||||
}
|
||||
c.Created = created
|
||||
updated, err := time.Parse(time.RFC3339, client["updated"].(string))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Errorf("failed to parse time")
|
||||
continue
|
||||
}
|
||||
c.Updated = updated
|
||||
|
||||
err = storage.Serialize(c.Id, c)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Errorf("failed to Serialize client")
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := s["presharedKey"]; ok {
|
||||
server := &model.Server{}
|
||||
|
||||
server.Address = make([]string, 0)
|
||||
server.Address = make([]string, 0)
|
||||
for _, address := range s["address"].([]interface{}) {
|
||||
server.Address = append(server.Address, address.(string))
|
||||
}
|
||||
server.ListenPort = int(s["listenPort"].(float64))
|
||||
server.PrivateKey = s["privateKey"].(string)
|
||||
server.PublicKey = s["publicKey"].(string)
|
||||
server.Endpoint = s["endpoint"].(string)
|
||||
server.PersistentKeepalive = int(s["persistentKeepalive"].(float64))
|
||||
server.Dns = make([]string, 0)
|
||||
for _, address := range s["dns"].([]interface{}) {
|
||||
server.Dns = append(server.Dns, address.(string))
|
||||
}
|
||||
if val, ok := s["preUp"]; ok {
|
||||
server.PreUp = val.(string)
|
||||
}
|
||||
if val, ok := s["postUp"]; ok {
|
||||
server.PostUp = val.(string)
|
||||
}
|
||||
if val, ok := s["preDown"]; ok {
|
||||
server.PreDown = val.(string)
|
||||
}
|
||||
if val, ok := s["postDown"]; ok {
|
||||
server.PostDown = val.(string)
|
||||
}
|
||||
created, err := time.Parse(time.RFC3339, s["created"].(string))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Errorf("failed to parse time")
|
||||
}
|
||||
server.Created = created
|
||||
updated, err := time.Parse(time.RFC3339, s["updated"].(string))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Errorf("failed to parse time")
|
||||
}
|
||||
server.Updated = updated
|
||||
|
||||
err = storage.Serialize("server.json", server)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Errorf("failed to Serialize server")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readClients() ([]map[string]interface{}, error) {
|
||||
clients := make([]map[string]interface{}, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
func deserialize(id string) (map[string]interface{}, error) {
|
||||
path := filepath.Join(os.Getenv("WG_CONF_DIR"), id)
|
||||
|
||||
data, err := util.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var d map[string]interface{}
|
||||
err = json.Unmarshal(data, &d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
166
core/server.go
Normal file
166
core/server.go
Normal file
@ -0,0 +1,166 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
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/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()
|
||||
|
||||
server.Endpoint = "wireguard.example.com:123"
|
||||
server.ListenPort = 51820
|
||||
|
||||
server.Address = make([]string, 0)
|
||||
server.Address = append(server.Address, "fd9f:6666::10:6:6:1/64")
|
||||
server.Address = append(server.Address, "10.6.6.1/24")
|
||||
|
||||
server.Dns = make([]string, 0)
|
||||
server.Dns = append(server.Dns, "fd9f::10:0:0:2")
|
||||
server.Dns = append(server.Dns, "10.0.0.2")
|
||||
|
||||
server.PersistentKeepalive = 16
|
||||
server.Mtu = 0
|
||||
server.PreUp = "echo WireGuard PreUp"
|
||||
server.PostUp = "echo WireGuard PostUp"
|
||||
server.PreDown = "echo WireGuard PreDown"
|
||||
server.PostDown = "echo WireGuard PostDown"
|
||||
server.Created = time.Now().UTC()
|
||||
server.Updated = server.Created
|
||||
|
||||
err = storage.Serialize("server.json", server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// server.json was missing, dump wg config after creation
|
||||
err = UpdateServerConfigWg()
|
||||
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
|
||||
}
|
||||
|
||||
// check if server is valid
|
||||
errs := server.IsValid()
|
||||
if len(errs) != 0 {
|
||||
for _, err := range errs {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("server validation error")
|
||||
}
|
||||
return nil, errors.New("failed to validate server")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// GetAllReservedIps the list of all reserved IPs, client and server
|
||||
func GetAllReservedIps() ([]string, error) {
|
||||
clients, err := ReadClients()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server, err := ReadServer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reserverIps := make([]string, 0)
|
||||
|
||||
for _, client := range clients {
|
||||
for _, cidr := range client.Address {
|
||||
ip, err := util.GetIpFromCidr(cidr)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
"cidr": cidr,
|
||||
}).Error("failed to ip from cidr")
|
||||
} else {
|
||||
reserverIps = append(reserverIps, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, cidr := range server.Address {
|
||||
ip, err := util.GetIpFromCidr(cidr)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
"cidr": err,
|
||||
}).Error("failed to ip from cidr")
|
||||
} else {
|
||||
reserverIps = append(reserverIps, ip)
|
||||
}
|
||||
}
|
||||
|
||||
return reserverIps, nil
|
||||
}
|
17
go.mod
Normal file
17
go.mod
Normal file
@ -0,0 +1,17 @@
|
||||
module gitlab.127-0-0-1.fr/vx3r/wg-gen-web
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e
|
||||
github.com/gin-contrib/cors v1.3.1
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
|
||||
github.com/gin-gonic/gin v1.6.2
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/sirupsen/logrus v1.5.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200324154536-ceff61240acf
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
)
|
118
main.go
Normal file
118
main.go
Normal file
@ -0,0 +1,118 @@
|
||||
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/core"
|
||||
"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() {
|
||||
log.Infof("Starting Wg Gen Web version: %s", util.Version)
|
||||
|
||||
// load .env environment variables
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Fatal("failed to load .env file")
|
||||
}
|
||||
|
||||
// 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 create directory")
|
||||
}
|
||||
}
|
||||
|
||||
// check if server.json exists otherwise create it with default values
|
||||
if !util.FileExists(filepath.Join(os.Getenv("WG_CONF_DIR"), "server.json")) {
|
||||
_, err = core.ReadServer()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Fatal("server.json doesnt not exists and can not read it")
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("GIN_MODE") == "debug" {
|
||||
// set gin release debug
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
// set gin release mode
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
// disable console color
|
||||
gin.DisableConsoleColor()
|
||||
// log level info
|
||||
log.SetLevel(log.InfoLevel)
|
||||
}
|
||||
|
||||
// migrate
|
||||
err = core.MigrateInitialStructChange()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Fatal("failed to migrate initial struct changes")
|
||||
}
|
||||
err = core.MigratePresharedKey()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Fatal("failed to migrate preshared key struct changes")
|
||||
}
|
||||
|
||||
// dump wg config file
|
||||
err = core.UpdateServerConfigWg()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Fatal("failed to dump wg config file")
|
||||
}
|
||||
|
||||
// creates a gin router with default middleware: logger and recovery (crash-free) middleware
|
||||
app := gin.Default()
|
||||
|
||||
// cors middleware
|
||||
config := cors.DefaultConfig()
|
||||
config.AllowAllOrigins = true
|
||||
app.Use(cors.New(config))
|
||||
|
||||
// protection middleware
|
||||
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")
|
||||
}
|
||||
}
|
64
model/client.go
Normal file
64
model/client.go
Normal file
@ -0,0 +1,64 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client structure
|
||||
type Client struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Enable bool `json:"enable"`
|
||||
IgnorePersistentKeepalive bool `json:"ignorePersistentKeepalive"`
|
||||
PresharedKey string `json:"presharedKey"`
|
||||
AllowedIPs []string `json:"allowedIPs"`
|
||||
Address []string `json:"address"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
func (a Client) IsValid() []error {
|
||||
errs := make([]error, 0)
|
||||
|
||||
// check if the name empty
|
||||
if a.Name == "" {
|
||||
errs = append(errs, fmt.Errorf("name est requis"))
|
||||
}
|
||||
// check the name field is between 3 to 40 chars
|
||||
if len(a.Name) < 2 || len(a.Name) > 40 {
|
||||
errs = append(errs, fmt.Errorf("name field must be between 2-40 chars"))
|
||||
}
|
||||
// email is not required, but if provided must match regex
|
||||
if a.Email != "" {
|
||||
if !util.RegexpEmail.MatchString(a.Email) {
|
||||
errs = append(errs, fmt.Errorf("email %s is invalid", a.Email))
|
||||
}
|
||||
}
|
||||
// check if the allowedIPs empty
|
||||
if len(a.AllowedIPs) == 0 {
|
||||
errs = append(errs, fmt.Errorf("allowedIPs field est requis"))
|
||||
}
|
||||
// check if the allowedIPs are valid
|
||||
for _, allowedIP := range a.AllowedIPs {
|
||||
if !util.IsValidCidr(allowedIP) {
|
||||
errs = append(errs, fmt.Errorf("allowedIP %s is invalid", allowedIP))
|
||||
}
|
||||
}
|
||||
// check if the address empty
|
||||
if len(a.Address) == 0 {
|
||||
errs = append(errs, fmt.Errorf("address field est requis"))
|
||||
}
|
||||
// check if the address are valid
|
||||
for _, address := range a.Address {
|
||||
if !util.IsValidCidr(address) {
|
||||
errs = append(errs, fmt.Errorf("address %s is invalid", address))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
64
model/server.go
Normal file
64
model/server.go
Normal file
@ -0,0 +1,64 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Server structure
|
||||
type Server struct {
|
||||
Address []string `json:"address"`
|
||||
ListenPort int `json:"listenPort"`
|
||||
Mtu int `json:"mtu"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
PersistentKeepalive int `json:"persistentKeepalive"`
|
||||
Dns []string `json:"dns"`
|
||||
PreUp string `json:"preUp"`
|
||||
PostUp string `json:"postUp"`
|
||||
PreDown string `json:"preDown"`
|
||||
PostDown string `json:"postDown"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
func (a Server) IsValid() []error {
|
||||
errs := make([]error, 0)
|
||||
|
||||
// check if the address empty
|
||||
if len(a.Address) == 0 {
|
||||
errs = append(errs, fmt.Errorf("address est requis"))
|
||||
}
|
||||
// check if the address are valid
|
||||
for _, address := range a.Address {
|
||||
if !util.IsValidCidr(address) {
|
||||
errs = append(errs, fmt.Errorf("address %s is invalid", address))
|
||||
}
|
||||
}
|
||||
// check if the listenPort is valid
|
||||
if a.ListenPort < 0 || a.ListenPort > 65535 {
|
||||
errs = append(errs, fmt.Errorf("listenPort %s is invalid", a.ListenPort))
|
||||
}
|
||||
// check if the endpoint empty
|
||||
if a.Endpoint == "" {
|
||||
errs = append(errs, fmt.Errorf("endpoint est requis"))
|
||||
}
|
||||
// check if the persistentKeepalive is valid
|
||||
if a.PersistentKeepalive < 0 {
|
||||
errs = append(errs, fmt.Errorf("persistentKeepalive %d is invalid", a.PersistentKeepalive))
|
||||
}
|
||||
// check if the mtu is valid
|
||||
if a.Mtu < 0 {
|
||||
errs = append(errs, fmt.Errorf("MTU %d is invalid", a.PersistentKeepalive))
|
||||
}
|
||||
// check if the address are valid
|
||||
for _, dns := range a.Dns {
|
||||
if !util.IsValidIp(dns) {
|
||||
errs = append(errs, fmt.Errorf("dns %s is invalid", dns))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
47
storage/file.go
Normal file
47
storage/file.go
Normal 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
|
||||
}
|
311
template/template.go
Normal file
311
template/template.go
Normal file
@ -0,0 +1,311 @@
|
||||
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"
|
||||
)
|
||||
|
||||
var (
|
||||
emailTpl = `
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<!--[if gte mso 9]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="format-detection" content="date=no" />
|
||||
<meta name="format-detection" content="address=no" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
|
||||
<!--<![endif]-->
|
||||
<title>Email Template</title>
|
||||
<!--[if gte mso 9]>
|
||||
<style type="text/css" media="all">
|
||||
sup { font-size: 100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
|
||||
<style type="text/css" media="screen">
|
||||
/* Linked Styles */
|
||||
body { padding:0 !important; margin:0 !important; display:block !important; min-width:100% !important; width:100% !important; background:#001736; -webkit-text-size-adjust:none }
|
||||
a { color:#66c7ff; text-decoration:none }
|
||||
p { padding:0 !important; margin:0 !important }
|
||||
img { -ms-interpolation-mode: bicubic; /* Allow smoother rendering of resized image in Internet Explorer */ }
|
||||
.mcnPreviewText { display: none !important; }
|
||||
|
||||
|
||||
/* Mobile styles */
|
||||
@media only screen and (max-device-width: 480px), only screen and (max-width: 480px) {
|
||||
.mobile-shell { width: 100% !important; min-width: 100% !important; }
|
||||
.bg { background-size: 100% auto !important; -webkit-background-size: 100% auto !important; }
|
||||
|
||||
.text-header,
|
||||
.m-center { text-align: center !important; }
|
||||
|
||||
.center { margin: 0 auto !important; }
|
||||
.container { padding: 20px 10px !important }
|
||||
|
||||
.td { width: 100% !important; min-width: 100% !important; }
|
||||
|
||||
.m-br-15 { height: 15px !important; }
|
||||
.p30-15 { padding: 30px 15px !important; }
|
||||
|
||||
.m-td,
|
||||
.m-hide { display: none !important; width: 0 !important; height: 0 !important; font-size: 0 !important; line-height: 0 !important; min-height: 0 !important; }
|
||||
|
||||
.m-block { display: block !important; }
|
||||
|
||||
.fluid-img img { width: 100% !important; max-width: 100% !important; height: auto !important; }
|
||||
|
||||
.column,
|
||||
.column-top,
|
||||
.column-empty,
|
||||
.column-empty2,
|
||||
.column-dir-top { float: left !important; width: 100% !important; display: block !important; }
|
||||
|
||||
.column-empty { padding-bottom: 10px !important; }
|
||||
.column-empty2 { padding-bottom: 30px !important; }
|
||||
|
||||
.content-spacing { width: 15px !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="body" style="padding:0 !important; margin:0 !important; display:block !important; min-width:100% !important; width:100% !important; background:#001736; -webkit-text-size-adjust:none;">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="#001736">
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table width="650" border="0" cellspacing="0" cellpadding="0" class="mobile-shell">
|
||||
<tr>
|
||||
<td class="td container" style="width:650px; min-width:650px; font-size:0pt; line-height:0pt; margin:0; font-weight:normal; padding:55px 0px;">
|
||||
|
||||
<!-- Article / Image On The Left - Copy On The Right -->
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="padding-bottom: 10px;">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="tbrr p30-15" style="padding: 60px 30px; border-radius:26px 26px 0px 0px;" bgcolor="#12325c">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<th class="column-top" width="280" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="fluid-img" style="font-size:0pt; line-height:0pt; text-align:left;"><img src="cid:{{.QrcodePngName}}" width="280" height="210" border="0" alt="" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
<th class="column-empty2" width="30" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;"></th>
|
||||
<th class="column-top" width="280" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="h4 pb20" style="color:#ffffff; font-family:'Muli', Arial,sans-serif; font-size:20px; line-height:28px; text-align:left; padding-bottom:20px;">Hello</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text pb20" style="color:#ffffff; font-family:Arial,sans-serif; font-size:14px; line-height:26px; text-align:left; padding-bottom:20px;">Vous avez probablement demandé une configuration VPN. Voici la configuration <strong>{{.Client.Name}}</strong> créée le <strong>{{.Client.Created.Format "02/01/2006 15:4"}}</strong>. Scannez le Qrcode ou ouvrez le fichier de configuration joint dans le client VPN.</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- END Article / Image On The Left - Copy On The Right -->
|
||||
|
||||
<!-- Two Columns / Articles -->
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="padding-bottom: 10px;">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="#0e264b">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="p30-15" style="padding: 50px 30px;">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="h3 pb20" style="color:#ffffff; font-family:'Muli', Arial,sans-serif; font-size:25px; line-height:32px; text-align:left; padding-bottom:20px;">A propos de WireGuard</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text pb20" style="color:#ffffff; font-family:Arial,sans-serif; font-size:14px; line-height:26px; text-align:left; padding-bottom:20px;">WireGuard est un VPN extrêmement simple, rapide et moderne qui utilise un chiffrement de pointe. Il vise à être plus rapide, plus simple, plus léger et plus utile tout en évitant le casse-tête. Il est beaucoup plus performant comparé à OpenVPN.</td>
|
||||
</tr>
|
||||
<!-- Button -->
|
||||
<tr>
|
||||
<td align="left">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="blue-button text-button" style="background:#66c7ff; color:#c1cddc; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Télécharger WireGuard VPN Client</span></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END Button -->
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- END Two Columns / Articles -->
|
||||
|
||||
<!-- Footer -->
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#0e264b">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="text-footer1 pb10" style="color:#c1cddc; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">wg-webui-fr - Générateur de configuration simple basé sur le Web pour WireGuard</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-footer2" style="color:#8297b3; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="https://gitea.xoyize.xyz/yako/wg-webui-fr" target="_blank" class="link" style="color:#66c7ff; text-decoration:none;"><span class="link" style="color:#66c7ff; text-decoration:none;">Informations sur Gitea</span></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- END Footer -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
clientTpl = `[Interface]
|
||||
Address = {{ StringsJoin .Client.Address ", " }}
|
||||
PrivateKey = {{ .Client.PrivateKey }}
|
||||
{{ if ne (len .Server.Dns) 0 -}}
|
||||
DNS = {{ StringsJoin .Server.Dns ", " }}
|
||||
{{- end }}
|
||||
{{ if ne .Server.Mtu 0 -}}
|
||||
MTU = {{.Server.Mtu}}
|
||||
{{- end}}
|
||||
[Peer]
|
||||
PublicKey = {{ .Server.PublicKey }}
|
||||
PresharedKey = {{ .Client.PresharedKey }}
|
||||
AllowedIPs = {{ StringsJoin .Client.AllowedIPs ", " }}
|
||||
Endpoint = {{ .Server.Endpoint }}
|
||||
{{ if and (ne .Server.PersistentKeepalive 0) (not .Client.IgnorePersistentKeepalive) -}}
|
||||
PersistentKeepalive = {{.Server.PersistentKeepalive}}
|
||||
{{- end}}
|
||||
`
|
||||
|
||||
wgTpl = `# Updated: {{ .Server.Updated }} / Created: {{ .Server.Created }}
|
||||
[Interface]
|
||||
{{- range .Server.Address }}
|
||||
Address = {{ . }}
|
||||
{{- end }}
|
||||
ListenPort = {{ .Server.ListenPort }}
|
||||
PrivateKey = {{ .Server.PrivateKey }}
|
||||
{{ if ne .Server.Mtu 0 -}}
|
||||
MTU = {{.Server.Mtu}}
|
||||
{{- end}}
|
||||
PreUp = {{ .Server.PreUp }}
|
||||
PostUp = {{ .Server.PostUp }}
|
||||
PreDown = {{ .Server.PreDown }}
|
||||
PostDown = {{ .Server.PostDown }}
|
||||
{{- range .Clients }}
|
||||
{{ if .Enable -}}
|
||||
# {{.Name}} / {{.Email}} / Updated: {{.Updated}} / Created: {{.Created}}
|
||||
[Peer]
|
||||
PublicKey = {{ .PublicKey }}
|
||||
PresharedKey = {{ .PresharedKey }}
|
||||
AllowedIPs = {{ StringsJoin .Address ", " }}
|
||||
{{- end }}
|
||||
{{ end }}`
|
||||
)
|
||||
|
||||
// DumpClientWg dump client wg config with go template
|
||||
func DumpClientWg(client *model.Client, server *model.Server) ([]byte, error) {
|
||||
t, err := template.New("client").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(clientTpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dump(t, struct {
|
||||
Client *model.Client
|
||||
Server *model.Server
|
||||
}{
|
||||
Client: client,
|
||||
Server: server,
|
||||
})
|
||||
}
|
||||
|
||||
// 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").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(wgTpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configDataWg, err := dump(t, struct {
|
||||
Clients []*model.Client
|
||||
Server *model.Server
|
||||
}{
|
||||
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) ([]byte, error) {
|
||||
t, err := template.New("email").Parse(emailTpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dump(t, struct {
|
||||
Client *model.Client
|
||||
QrcodePngName string
|
||||
}{
|
||||
Client: client,
|
||||
QrcodePngName: qrcodePngName,
|
||||
})
|
||||
}
|
||||
|
||||
func dump(tpl *template.Template, data interface{}) ([]byte, error) {
|
||||
var tplBuff bytes.Buffer
|
||||
|
||||
err := tpl.Execute(&tplBuff, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tplBuff.Bytes(), 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/).
|
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",
|
||||
"is-cidr": "^3.1.0",
|
||||
"moment": "^2.24.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-moment": "^4.1.0",
|
||||
"vue-router": "^3.1.6",
|
||||
"vuetify": "^2.2.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-router": "^4.2.3",
|
||||
"@vue/cli-service": "^4.2.3",
|
||||
"sass": "^1.26.3",
|
||||
"sass-loader": "^8.0.0",
|
||||
"vue-cli-plugin-vuetify": "^2.0.5",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"vuetify-loader": "^1.3.0"
|
||||
}
|
||||
}
|
BIN
ui/public/favicon.png
Normal file
BIN
ui/public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 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.png">
|
||||
<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>
|
97
ui/src/App.vue
Normal file
97
ui/src/App.vue
Normal file
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<v-app id="inspire">
|
||||
|
||||
<v-app-bar app>
|
||||
<img class="mr-3" :src="require('./assets/logo.png')" height="50" alt="Wg Gen Web"/>
|
||||
<v-toolbar-title to="/">Configuration Wireguard VPN</v-toolbar-title>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-toolbar-items>
|
||||
<v-btn to="/clients">
|
||||
Clients
|
||||
<v-icon right dark>mdi-account-network-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn to="/server">
|
||||
Serveur
|
||||
<v-icon right dark>mdi-vpn</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
|
||||
</v-app-bar>
|
||||
|
||||
<v-content>
|
||||
<v-container>
|
||||
<router-view />
|
||||
</v-container>
|
||||
<Notification v-bind:notification="notification"/>
|
||||
</v-content>
|
||||
|
||||
<v-footer app>
|
||||
<v-row justify="start" no-gutters>
|
||||
<v-col cols="12" lg="6" md="12" sm="12">
|
||||
<div :align="$vuetify.breakpoint.smAndDown ? 'center' : 'left'">
|
||||
<small>{{ new Date().getFullYear() }} </small>
|
||||
<small><a class="pr-1 pl-1" href="http://www.wtfpl.net/" target="_blank">Licence WTFPL</a></small>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row justify="end" no-gutters>
|
||||
<v-col cols="12" lg="6" md="12" sm="12">
|
||||
<div :align="$vuetify.breakpoint.smAndDown ? 'center' : 'right'">
|
||||
<small>Sources </small>
|
||||
<a href="https://gitea.rnmkcy.eu/yann/wg-webui-fr">Gitea</a>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-footer>
|
||||
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ApiService} from "./services/ApiService";
|
||||
import Notification from './components/Notification'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
||||
components: {
|
||||
Notification
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
api: null,
|
||||
version:'N/A',
|
||||
notification: {
|
||||
show: false,
|
||||
color: '',
|
||||
text: '',
|
||||
},
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
this.api = new ApiService();
|
||||
this.getVersion()
|
||||
},
|
||||
|
||||
created () {
|
||||
this.$vuetify.theme.dark = true
|
||||
},
|
||||
|
||||
methods: {
|
||||
getVersion() {
|
||||
this.api.get('/server/version').then((res) => {
|
||||
this.version = res.version;
|
||||
}).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>
|
BIN
ui/src/assets/logo.png
Normal file
BIN
ui/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
476
ui/src/components/Clients.vue
Normal file
476
ui/src/components/Clients.vue
Normal file
@ -0,0 +1,476 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<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="startAddClient"
|
||||
>
|
||||
Ajout nouveau client
|
||||
<v-icon right dark>mdi-account-multiple-plus-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item>
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="(client, i) in clients"
|
||||
:key="i"
|
||||
sm12 lg6
|
||||
>
|
||||
<v-card
|
||||
:color="client.enable ? '#1F7087' : 'warning'"
|
||||
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>Créé: {{ client.created | formatDate }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>Modifié: {{ client.updated | formatDate }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
|
||||
<v-list-item-avatar
|
||||
tile
|
||||
size="150"
|
||||
>
|
||||
<v-img :src="`${apiBaseUrl}/client/${client.id}/config?qrcode=true`"/>
|
||||
</v-list-item-avatar>
|
||||
</v-list-item>
|
||||
|
||||
<v-card-text class="text--primary">
|
||||
<v-chip
|
||||
v-for="(ip, i) in client.address"
|
||||
: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-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
text
|
||||
:href="`${apiBaseUrl}/client/${client.id}/config?qrcode=false`"
|
||||
v-on="on"
|
||||
>
|
||||
<span class="d-none d-lg-flex">Télécharger</span>
|
||||
<v-icon right dark>mdi-cloud-download-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Télécharger</span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
text
|
||||
@click.stop="startUpdateClient(client)"
|
||||
v-on="on"
|
||||
>
|
||||
<span class="d-none d-lg-flex">Editer</span>
|
||||
<v-icon right dark>mdi-square-edit-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Editer</span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
text
|
||||
@click="sendEmailClient(client.id)"
|
||||
v-on="on"
|
||||
>
|
||||
<span class="d-none d-lg-flex">Envoyer message</span>
|
||||
<v-icon right dark>mdi-email-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Envoyer message</span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
text
|
||||
@click="deleteClient(client)"
|
||||
v-on="on"
|
||||
>
|
||||
<span class="d-none d-lg-flex">Supprimer</span>
|
||||
<v-icon right dark>mdi-trash-can-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Supprimer</span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-spacer/>
|
||||
<v-tooltip right>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-switch
|
||||
dark
|
||||
v-on="on"
|
||||
color="success"
|
||||
v-model="client.enable"
|
||||
v-on:change="updateClient(client)"
|
||||
/>
|
||||
</template>
|
||||
<span> {{client.enable ? 'Désactiver' : 'Activer'}} ce client</span>
|
||||
</v-tooltip>
|
||||
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-dialog
|
||||
v-if="client"
|
||||
v-model="dialogAddClient"
|
||||
max-width="550"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">Ajout nouveau 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="Nom convivial"
|
||||
:rules="[ v => !!v || 'Nom client est requis', ]"
|
||||
required
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="client.email"
|
||||
label="Client messagerie"
|
||||
:rules="[ v => (/.+@.+\..+/.test(v) || v === '') || 'Adresse non valide',]"
|
||||
/>
|
||||
<v-select
|
||||
v-model="client.address"
|
||||
:items="server.address"
|
||||
label="Adresse IP client choisie parmi les réseaux suivants"
|
||||
:rules="[ v => !!v || 'Réseau requis', ]"
|
||||
multiple
|
||||
chips
|
||||
persistent-hint
|
||||
required
|
||||
/>
|
||||
<v-combobox
|
||||
v-model="client.allowedIPs"
|
||||
chips
|
||||
hint="Saisir le CIDR IPv4 ou IPv6 et appuyez sur touche Entrée"
|
||||
label="IP autorisés"
|
||||
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>
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
|
||||
<v-switch
|
||||
v-model="client.enable"
|
||||
color="red"
|
||||
inset
|
||||
:label="client.enable ? 'Activer client après création': 'Désactiver client après création'"
|
||||
/>
|
||||
<v-switch
|
||||
v-model="client.ignorePersistentKeepalive"
|
||||
color="red"
|
||||
inset
|
||||
:label="'Ignorer le keepalive persistant global: ' + (client.ignorePersistentKeepalive ? 'Oui': 'NON')"
|
||||
/>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
:disabled="!valid"
|
||||
color="success"
|
||||
@click="addClient(client)"
|
||||
>
|
||||
Valider
|
||||
<v-icon right dark>mdi-check-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="dialogAddClient = false"
|
||||
>
|
||||
Abandon
|
||||
<v-icon right dark>mdi-close-circle-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog
|
||||
v-if="client"
|
||||
v-model="dialogEditClient"
|
||||
max-width="550"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">Edition 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="Nom convivial"
|
||||
:rules="[ v => !!v || 'Nom client est requis',]"
|
||||
required
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="client.email"
|
||||
label="Messagerie"
|
||||
:rules="[ v => (/.+@.+\..+/.test(v) || v === '') || 'Adresse non valide',]"
|
||||
required
|
||||
/>
|
||||
<v-combobox
|
||||
v-model="client.address"
|
||||
chips
|
||||
hint="Saisir le CIDR IPv4 ou IPv6 et appuyez sur touche Entrée"
|
||||
label="Adresses"
|
||||
multiple
|
||||
dark
|
||||
>
|
||||
<template v-slot:selection="{ attrs, item, select, selected }">
|
||||
<v-chip
|
||||
v-bind="attrs"
|
||||
:input-value="selected"
|
||||
close
|
||||
@click="select"
|
||||
@click:close="client.address.splice(client.address.indexOf(item), 1)"
|
||||
>
|
||||
<strong>{{ item }}</strong>
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
<v-combobox
|
||||
v-model="client.allowedIPs"
|
||||
chips
|
||||
hint="Saisir le CIDR IPv4 ou IPv6 et appuyez sur touche Entrée"
|
||||
label="IP autorisés"
|
||||
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>
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
<v-switch
|
||||
v-model="client.ignorePersistentKeepalive"
|
||||
color="red"
|
||||
inset
|
||||
:label="'Ignorer le keepalive persistant global: ' + (client.ignorePersistentKeepalive ? 'Oui': 'NON')"
|
||||
/>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
:disabled="!valid"
|
||||
color="success"
|
||||
@click="updateClient(client)"
|
||||
>
|
||||
Valider
|
||||
<v-icon right dark>mdi-check-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="dialogEditClient = false"
|
||||
>
|
||||
Abandon
|
||||
<v-icon right dark>mdi-close-circle-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<Notification v-bind:notification="notification"/>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import {ApiService, API_BASE_URL} from '../services/ApiService'
|
||||
import Notification from '../components/Notification'
|
||||
|
||||
export default {
|
||||
name: 'Clients',
|
||||
|
||||
components: {
|
||||
Notification
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
api: null,
|
||||
apiBaseUrl: API_BASE_URL,
|
||||
clients: [],
|
||||
notification: {
|
||||
show: false,
|
||||
color: '',
|
||||
text: '',
|
||||
},
|
||||
dialogAddClient: false,
|
||||
dialogEditClient: false,
|
||||
client: null,
|
||||
server: null,
|
||||
valid: false,
|
||||
}),
|
||||
|
||||
mounted () {
|
||||
this.api = new ApiService();
|
||||
this.getClients();
|
||||
this.getServer()
|
||||
},
|
||||
|
||||
methods: {
|
||||
getClients() {
|
||||
this.api.get('/client').then((res) => {
|
||||
this.clients = res
|
||||
}).catch((e) => {
|
||||
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||
});
|
||||
},
|
||||
|
||||
getServer() {
|
||||
this.api.get('/server').then((res) => {
|
||||
this.server = res;
|
||||
}).catch((e) => {
|
||||
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||
});
|
||||
},
|
||||
|
||||
startAddClient() {
|
||||
this.dialogAddClient = true;
|
||||
this.client = {
|
||||
name: "",
|
||||
email: "",
|
||||
enable: true,
|
||||
allowedIPs: ["0.0.0.0/0", "::/0"],
|
||||
address: this.server.address,
|
||||
}
|
||||
},
|
||||
addClient(client) {
|
||||
if (client.allowedIPs.length < 1) {
|
||||
this.notify('error', 'Veuillez fournir au moins une adresse CIDR valide pour les adresses IP autorisées du client.');
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < client.allowedIPs.length; i++){
|
||||
if (this.$isCidr(client.allowedIPs[i]) === 0) {
|
||||
this.notify('error', 'Un CIDR invalide a été détecté, veuillez corriger avant de soumettre');
|
||||
return
|
||||
}
|
||||
}
|
||||
this.dialogAddClient = false;
|
||||
|
||||
this.api.post('/client', client).then((res) => {
|
||||
this.notify('success', `Client ${res.name} ajouté avec succès`);
|
||||
this.getClients()
|
||||
}).catch((e) => {
|
||||
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||
});
|
||||
},
|
||||
|
||||
deleteClient(client) {
|
||||
if(confirm(`Voulez-vous vraiment supprimer ${client.name} ?`)){
|
||||
this.api.delete(`/client/${client.id}`).then((res) => {
|
||||
this.notify('success', "Client supprimé avec succès");
|
||||
this.getClients()
|
||||
}).catch((e) => {
|
||||
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
sendEmailClient(id) {
|
||||
this.api.get(`/client/${id}/email`).then((res) => {
|
||||
this.notify('success', "Courriel envoyé avec succès");
|
||||
this.getClients()
|
||||
}).catch((e) => {
|
||||
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||
});
|
||||
},
|
||||
|
||||
startUpdateClient(client) {
|
||||
this.client = client;
|
||||
this.dialogEditClient = true;
|
||||
},
|
||||
updateClient(client) {
|
||||
// check allowed IPs
|
||||
if (client.allowedIPs.length < 1) {
|
||||
this.notify('error', 'Veuillez fournir au moins une adresse CIDR valide pour les adresses IP autorisées du client');
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < client.allowedIPs.length; i++){
|
||||
if (this.$isCidr(client.allowedIPs[i]) === 0) {
|
||||
this.notify('error', 'Un CIDR invalide a été détecté, veuillez corriger avant de soumettre');
|
||||
return
|
||||
}
|
||||
}
|
||||
// check address
|
||||
if (client.address.length < 1) {
|
||||
this.notify('error', 'Veuillez fournir au moins une adresse CIDR valide pour le client');
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < client.address.length; i++){
|
||||
if (this.$isCidr(client.address[i]) === 0) {
|
||||
this.notify('error', 'Un CIDR invalide a été détecté, veuillez corriger avant de soumettre');
|
||||
return
|
||||
}
|
||||
}
|
||||
// all good, submit
|
||||
this.dialogEditClient = false;
|
||||
|
||||
this.api.patch(`/client/${client.id}`, client).then((res) => {
|
||||
this.notify('success', `Client ${res.name} mise à jour réussie`);
|
||||
this.getClients()
|
||||
}).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>
|
25
ui/src/components/Notification.vue
Normal file
25
ui/src/components/Notification.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<v-snackbar
|
||||
v-model="notification.show"
|
||||
:right="true"
|
||||
:top="true"
|
||||
:color="notification.color"
|
||||
>
|
||||
{{ notification.text }}
|
||||
<v-btn
|
||||
dark
|
||||
text
|
||||
@click="notification.show = false"
|
||||
>
|
||||
Fermer
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'Notification',
|
||||
props: ['notification'],
|
||||
data: () => ({
|
||||
}),
|
||||
};
|
||||
</script>
|
235
ui/src/components/Server.vue
Normal file
235
ui/src/components/Server.vue
Normal file
@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<v-container v-if="server">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card dark>
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="headline">Interface configuration serveur</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<div class="d-flex flex-no-wrap justify-space-between">
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="server.publicKey"
|
||||
label="Clé publique"
|
||||
disabled
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="server.listenPort"
|
||||
type="number"
|
||||
:rules="[
|
||||
v => !!v || 'Le port en écoute est requis',
|
||||
]"
|
||||
label="Port en écoute"
|
||||
required
|
||||
/>
|
||||
<v-combobox
|
||||
v-model="server.address"
|
||||
chips
|
||||
hint="Saisir le CIDR IPv4 ou IPv6 et appuyez sur touche Entrée"
|
||||
label="Adresse interfaces serveur"
|
||||
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>
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-card dark>
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="headline">Configuration globale du client</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<div class="d-flex flex-no-wrap justify-space-between">
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="server.endpoint"
|
||||
label="Point accès public connexion client"
|
||||
:rules="[
|
||||
v => !!v || 'Point accès public connexion client est requis',
|
||||
]"
|
||||
required
|
||||
/>
|
||||
<v-combobox
|
||||
v-model="server.dns"
|
||||
chips
|
||||
hint="Saisir adresse IPv4 ou IPv6 et appui sur touche Entrée"
|
||||
label="Serveurs DNS pour les 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>
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model="server.mtu"
|
||||
label="Définir le MTU global"
|
||||
hint="Laisser à 0 et laisser wg-quick gérer le MTU"
|
||||
/>
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model="server.persistentKeepalive"
|
||||
label="Keepalive persistant"
|
||||
hint="Laissez à 0 pour ne pas spécifier un keepalive persistant"
|
||||
/>
|
||||
</v-col>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card dark>
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="headline">Interface configuration hooks</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<div class="d-flex flex-no-wrap justify-space-between">
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="server.preUp"
|
||||
label="PreUp: extraits de scripts qui seront exécutés par bash avant la mise en place de l'interface"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="server.postUp"
|
||||
label="PostUp: extraits de scripts qui seront exécutés par bash après la mise en place de l'interface"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="server.preDown"
|
||||
label="PreDown: extraits de scripts qui seront exécutés par bash avant la mise en place de l'interface"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="server.postDown "
|
||||
label="PostDown : extraits de scripts qui seront exécutés par bash après la mise en place de l'interface"
|
||||
/>
|
||||
</v-col>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-divider dark/>
|
||||
<v-btn
|
||||
class="ma-2"
|
||||
color="success"
|
||||
:href="`${apiBaseUrl}/server/config`"
|
||||
>
|
||||
Télécharger la configuration du serveur
|
||||
<v-icon right dark>mdi-cloud-download-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
class="ma-2"
|
||||
color="warning"
|
||||
@click="updateServer"
|
||||
>
|
||||
Mise à jour configuration serveur
|
||||
<v-icon right dark>mdi-update</v-icon>
|
||||
</v-btn>
|
||||
<v-divider dark/>
|
||||
</v-row>
|
||||
<Notification v-bind:notification="notification"/>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import {API_BASE_URL, ApiService} from "../services/ApiService";
|
||||
import Notification from '../components/Notification'
|
||||
|
||||
export default {
|
||||
name: 'Server',
|
||||
|
||||
components: {
|
||||
Notification
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
api: null,
|
||||
server: null,
|
||||
apiBaseUrl: API_BASE_URL,
|
||||
notification: {
|
||||
show: false,
|
||||
color: '',
|
||||
text: '',
|
||||
},
|
||||
}),
|
||||
|
||||
mounted () {
|
||||
this.api = new ApiService();
|
||||
this.getServer()
|
||||
},
|
||||
|
||||
methods: {
|
||||
getServer() {
|
||||
this.api.get('/server').then((res) => {
|
||||
this.server = res;
|
||||
}).catch((e) => {
|
||||
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||
});
|
||||
},
|
||||
updateServer () {
|
||||
// convert int values
|
||||
this.server.listenPort = parseInt(this.server.listenPort, 10);
|
||||
this.server.persistentKeepalive = parseInt(this.server.persistentKeepalive, 10);
|
||||
this.server.mtu = parseInt(this.server.mtu, 10);
|
||||
|
||||
// check server addresses
|
||||
if (this.server.address.length < 1) {
|
||||
this.notify('error', 'Veuillez fournir au moins une adresse CIDR valide pour le serveur');
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this.server.address.length; i++){
|
||||
if (this.$isCidr(this.server.address[i]) === 0) {
|
||||
this.notify('error', `Un CIDR invalide a été détecté, veuillez corriger ${this.server.address[i]} avant de soumettre`);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// check DNS correct
|
||||
for (let i = 0; i < this.server.dns.length; i++){
|
||||
if (this.$isCidr(this.server.dns[i] + '/32') === 0) {
|
||||
this.notify('error', `IP invalide détectée, veuillez corriger ${this.server.dns[i]} avant de soumettre`);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.api.patch('/server', this.server).then((res) => {
|
||||
this.notify('success', "Mise à jour du serveur réussie");
|
||||
this.server = res;
|
||||
}).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>
|
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/moment';
|
||||
import './plugins/cidr'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
vuetify,
|
||||
render: function (h) { return h(App) }
|
||||
}).$mount('#app')
|
11
ui/src/plugins/cidr.js
Normal file
11
ui/src/plugins/cidr.js
Normal 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);
|
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('en');
|
||||
|
||||
Vue.use(VueMoment, {
|
||||
moment
|
||||
});
|
||||
// $moment() accessible in project
|
||||
|
||||
Vue.filter('formatDate', function (value) {
|
||||
if (!value) return '';
|
||||
return moment(String(value)).format('DD/MM/YYYY 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({
|
||||
});
|
36
ui/src/router/index.js
Normal file
36
ui/src/router/index.js
Normal file
@ -0,0 +1,36 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'index',
|
||||
component: function () {
|
||||
return import(/* webpackChunkName: "Index" */ '../views/Index.vue')
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/clients',
|
||||
name: 'clients',
|
||||
component: function () {
|
||||
return import(/* webpackChunkName: "Clients" */ '../views/Clients.vue')
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/server',
|
||||
name: 'server',
|
||||
component: function () {
|
||||
return import(/* webpackChunkName: "Server" */ '../views/Server.vue')
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
});
|
||||
|
||||
export default router
|
40
ui/src/services/ApiService.js
Normal file
40
ui/src/services/ApiService.js
Normal file
@ -0,0 +1,40 @@
|
||||
import axios from 'axios'
|
||||
|
||||
let baseUrl = "/api/v1.0";
|
||||
if (process.env.NODE_ENV === "development"){
|
||||
baseUrl = process.env.VUE_APP_API_BASE_URL
|
||||
}
|
||||
|
||||
export const API_BASE_URL = baseUrl;
|
||||
|
||||
export class ApiService {
|
||||
get(resource) {
|
||||
return axios
|
||||
.get(`${API_BASE_URL}${resource}`)
|
||||
.then(response => response.data)
|
||||
};
|
||||
|
||||
post(resource, data) {
|
||||
return axios
|
||||
.post(`${API_BASE_URL}${resource}`, data)
|
||||
.then(response => response.data)
|
||||
};
|
||||
|
||||
put(resource, data) {
|
||||
return axios
|
||||
.put(`${API_BASE_URL}${resource}`, data)
|
||||
.then(response => response.data)
|
||||
};
|
||||
|
||||
patch(resource, data) {
|
||||
return axios
|
||||
.patch(`${API_BASE_URL}${resource}`, data)
|
||||
.then(response => response.data)
|
||||
};
|
||||
|
||||
delete(resource) {
|
||||
return axios
|
||||
.delete(`${API_BASE_URL}${resource}`)
|
||||
.then(response => response.data)
|
||||
};
|
||||
}
|
16
ui/src/views/Clients.vue
Normal file
16
ui/src/views/Clients.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<v-content>
|
||||
<Clients/>
|
||||
</v-content>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Clients from '../components/Clients'
|
||||
|
||||
export default {
|
||||
name: 'clients',
|
||||
components: {
|
||||
Clients
|
||||
}
|
||||
}
|
||||
</script>
|
10
ui/src/views/Index.vue
Normal file
10
ui/src/views/Index.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
created () {
|
||||
this.$router.replace({ name: 'clients' })
|
||||
}
|
||||
}
|
||||
</script>
|
16
ui/src/views/Server.vue
Normal file
16
ui/src/views/Server.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<v-content>
|
||||
<Server/>
|
||||
</v-content>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Server from '../components/Server'
|
||||
|
||||
export default {
|
||||
name: 'server',
|
||||
components: {
|
||||
Server
|
||||
}
|
||||
}
|
||||
</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"
|
||||
]
|
||||
};
|
135
util/util.go
Normal file
135
util/util.go
Normal file
@ -0,0 +1,135 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
RegexpEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||
// pushed build time
|
||||
Version string
|
||||
)
|
||||
|
||||
// ReadFile file content
|
||||
func ReadFile(path string) (bytes []byte, err error) {
|
||||
bytes, err = ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
// WriteFile content to file
|
||||
func WriteFile(path string, bytes []byte) (err error) {
|
||||
err = ioutil.WriteFile(path, bytes, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileExists check if file exists
|
||||
func FileExists(name string) bool {
|
||||
info, err := os.Stat(name)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
// DirectoryExists check if directory exists
|
||||
func DirectoryExists(name string) bool {
|
||||
info, err := os.Stat(name)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
// GetAvailableIp search for an available in cidr against a list of reserved ips
|
||||
func GetAvailableIp(cidr string, reserved []string) (string, error) {
|
||||
ip, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// this two addresses are not usable
|
||||
broadcastAddr := BroadcastAddr(ipnet).String()
|
||||
networkAddr := ipnet.IP.String()
|
||||
|
||||
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
|
||||
ok := true
|
||||
address := ip.String()
|
||||
for _, r := range reserved {
|
||||
if address == r {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if ok && address != networkAddr && address != broadcastAddr {
|
||||
return address, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("no more available address from cidr")
|
||||
}
|
||||
|
||||
// IsIPv6 check if given ip is IPv6
|
||||
func IsIPv6(address string) bool {
|
||||
ip := net.ParseIP(address)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() == nil
|
||||
}
|
||||
|
||||
// IsValidIp check if ip is valid
|
||||
func IsValidIp(ip string) bool {
|
||||
return net.ParseIP(ip) != nil
|
||||
}
|
||||
|
||||
// IsValidCidr check if CIDR is valid
|
||||
func IsValidCidr(cidr string) bool {
|
||||
_, _, err := net.ParseCIDR(cidr)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// GetIpFromCidr get ip from cidr
|
||||
func GetIpFromCidr(cidr string) (string, error) {
|
||||
ip, _, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ip.String(), 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastAddr returns the last address in the given network, or the broadcast address.
|
||||
func BroadcastAddr(n *net.IPNet) net.IP {
|
||||
// The golang net package doesn't make it easy to calculate the broadcast address. :(
|
||||
var broadcast net.IP
|
||||
if len(n.IP) == 4 {
|
||||
broadcast = net.ParseIP("0.0.0.0").To4()
|
||||
} else {
|
||||
broadcast = net.ParseIP("::")
|
||||
}
|
||||
for i := 0; i < len(n.IP); i++ {
|
||||
broadcast[i] = n.IP[i] | ^n.Mask[i]
|
||||
}
|
||||
return broadcast
|
||||
}
|
BIN
web-ui-a.png
Normal file
BIN
web-ui-a.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
BIN
web-ui.png
Normal file
BIN
web-ui.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Loading…
Reference in New Issue
Block a user