Merge branch 'master' into try-add-apikey

This commit is contained in:
Roman 2023-03-18 20:32:55 +03:00 committed by GitHub
commit c7a50ff779
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1220 additions and 220 deletions

View file

@ -25,3 +25,6 @@ docker-compose*
db db
assets assets
wireguard-ui wireguard-ui
# Examples
examples

View file

@ -40,20 +40,12 @@ jobs:
node-version: '14' node-version: '14'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
# prepare assets for go rice # prepare assets
- name: Prepare assets - name: Prepare assets
run: | run: |
chmod +x ./prepare_assets.sh chmod +x ./prepare_assets.sh
./prepare_assets.sh ./prepare_assets.sh
# get go rice tool
- name: Get go rice tool
run: go get github.com/GeertJohan/go.rice/rice
# run go rice embed
- name: Run go rice embed
run: ${HOME}/go/bin/rice embed-go
# build and make the releases # build and make the releases
- name: Build and make the releases - name: Build and make the releases
uses: wangyoucao577/go-release-action@master uses: wangyoucao577/go-release-action@master
@ -62,6 +54,7 @@ jobs:
goos: ${{ matrix.goos }} goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }} goarch: ${{ matrix.goarch }}
goversion: "https://dl.google.com/go/go1.16.1.linux-amd64.tar.gz" goversion: "https://dl.google.com/go/go1.16.1.linux-amd64.tar.gz"
pre_command: export CGO_ENABLED=0
binary_name: "wireguard-ui" binary_name: "wireguard-ui"
build_flags: -v build_flags: -v
ldflags: -X "main.appVersion=${{ env.APP_VERSION }}" -X "main.buildTime=${{ env.BUILD_TIME }}" -X main.gitCommit=${{ github.sha }} -X main.gitRef=${{ github.ref }} ldflags: -X "main.appVersion=${{ env.APP_VERSION }}" -X "main.buildTime=${{ env.BUILD_TIME }}" -X main.gitCommit=${{ github.sha }} -X main.gitRef=${{ github.ref }}

5
.gitignore vendored
View file

@ -16,8 +16,11 @@ wireguard-ui
vendor/ vendor/
assets/ assets/
node_modules/ node_modules/
rice-box.go
# IDEs # IDEs
.vscode .vscode
.idea .idea
# Examples
examples/docker-compose/config
examples/docker-compose/db

View file

@ -4,6 +4,7 @@ LABEL maintainer="Khanh Ngo <k@ndk.name"
ARG TARGETOS=linux ARG TARGETOS=linux
ARG TARGETARCH=amd64 ARG TARGETARCH=amd64
ARG COMMIT=
ARG BUILD_DEPENDENCIES="npm \ ARG BUILD_DEPENDENCIES="npm \
yarn" yarn"
@ -42,10 +43,6 @@ RUN mkdir -p assets/plugins && \
/build/node_modules/jquery-tags-input/ \ /build/node_modules/jquery-tags-input/ \
assets/plugins/ assets/plugins/
# Get go modules and build tool
RUN go mod download && \
go get github.com/GeertJohan/go.rice/rice
# Add sources # Add sources
COPY . /build COPY . /build
@ -53,8 +50,7 @@ COPY . /build
RUN cp -r /build/custom/ assets/ RUN cp -r /build/custom/ assets/
# Build # Build
RUN rice embed-go && \ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X main.gitCommit=${COMMIT}" -a -o wg-ui .
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -a -o wg-ui .
# Release stage # Release stage
FROM alpine:3.16 FROM alpine:3.16

View file

@ -35,15 +35,6 @@ volume mount points to work with your setup. Then run it like below:
docker-compose up docker-compose up
``` ```
Note:
- There is a Status page that needs docker to be able to access the network of the host in order to read the
wireguard interface stats. See the `cap_add` and `network_mode` options on the docker-compose.yaml
- Similarly, the `WGUI_MANAGE_START` and `WGUI_MANAGE_RESTART` settings need the same access, in order to restart the
wireguard interface.
- Because the `network_mode` is set to `host`, we don't need to specify the exposed ports. The app will listen on
port `5000` by default.
## Environment Variables ## Environment Variables
| Variable | Description | Default | | Variable | Description | Default |
@ -53,14 +44,16 @@ Note:
| `WGUI_USERNAME` | The username for the login page. Used for db initialization only | `admin` | | `WGUI_USERNAME` | The username for the login page. Used for db initialization only | `admin` |
| `WGUI_PASSWORD` | The password for the user on the login page. Will be hashed automatically. Used for db initialization only | `admin` | | `WGUI_PASSWORD` | The password for the user on the login page. Will be hashed automatically. Used for db initialization only | `admin` |
| `WGUI_PASSWORD_HASH` | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only | N/A | | `WGUI_PASSWORD_HASH` | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only | N/A |
| `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings where clients should connect to | Resolved to your public ip address |
| `WGUI_FAVICON_FILE_PATH` | The file path used as website favicon | Embedded WireGuard logo | | `WGUI_FAVICON_FILE_PATH` | The file path used as website favicon | Embedded WireGuard logo |
| `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings | Resolved to your public ip address | | `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings | Resolved to your public ip address |
| `WGUI_DNS` | The default DNS servers (comma-separated-list) used in the global settings | `1.1.1.1` | | `WGUI_DNS` | The default DNS servers (comma-separated-list) used in the global settings | `1.1.1.1` |
| `WGUI_MTU` | The default MTU used in global settings | `1450` | | `WGUI_MTU` | The default MTU used in global settings | `1450` |
| `WGUI_PERSISTENT_KEEPALIVE` | The default persistent keepalive for WireGuard in global settings | `15` | | `WGUI_PERSISTENT_KEEPALIVE` | The default persistent keepalive for WireGuard in global settings | `15` |
| `WGUI_FORWARD_MARK` | The default WireGuard forward mark | `0xca6c` | | `WGUI_FIREWALL_MARK` | The default WireGuard firewall mark | `0xca6c` (51820) |
| `WGUI_CONFIG_FILE_PATH` | The default WireGuard config file path used in global settings | `/etc/wireguard/wg0.conf` | | `WGUI_CONFIG_FILE_PATH` | The default WireGuard config file path used in global settings | `/etc/wireguard/wg0.conf` |
| `WG_CONF_TEMPLATE` | The custom `wg.conf` config file template. Please refer to our [default template](https://github.com/alikhanich/wireguard-ui/blob/master/templates/wg.conf) | N/A | | `WGUI_LOG_LEVEL` | The default log level. Possible values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF` | `INFO` |
| `WG_CONF_TEMPLATE` | The custom `wg.conf` config file template. Please refer to our [default template](https://github.com/ngoduykhanh/wireguard-ui/blob/master/templates/wg.conf) | N/A |
| `EMAIL_FROM_ADDRESS` | The sender email address | N/A | | `EMAIL_FROM_ADDRESS` | The sender email address | N/A |
| `EMAIL_FROM_NAME` | The sender name | `WireGuard UI` | | `EMAIL_FROM_NAME` | The sender name | `WireGuard UI` |
| `SENDGRID_API_KEY` | The SendGrid api key | N/A | | `SENDGRID_API_KEY` | The SendGrid api key | N/A |
@ -202,7 +195,13 @@ feature work.
Go to the project root directory and run the following command: Go to the project root directory and run the following command:
```sh ```sh
docker build -t wireguard-ui . docker build --build-arg=COMMIT=$(git rev-parse --short HEAD) -t wireguard-ui .
```
or
```sh
docker compose build --build-arg=COMMIT=$(git rev-parse --short HEAD)
``` ```
### Build binary file ### Build binary file
@ -213,18 +212,9 @@ Prepare the assets directory
./prepare_assets.sh ./prepare_assets.sh
``` ```
Then you can embed resources by generating Go source code Then build your executable
```sh
rice embed-go
go build -o wireguard-ui
```
Or, append resources to executable as zip file
```sh ```sh
go build -o wireguard-ui go build -o wireguard-ui
rice append --exec wireguard-ui
``` ```
## License ## License

View file

@ -58,6 +58,7 @@ function renderClientList(data) {
</div> </div>
<hr> <hr>
<span class="info-box-text"><i class="fas fa-user"></i> ${obj.Client.name}</span> <span class="info-box-text"><i class="fas fa-user"></i> ${obj.Client.name}</span>
<span class="info-box-text" style="display: none"><i class="fas fa-key"></i> ${obj.Client.public_key}</span>
<span class="info-box-text"><i class="fas fa-envelope"></i> ${obj.Client.email}</span> <span class="info-box-text"><i class="fas fa-envelope"></i> ${obj.Client.email}</span>
<span class="info-box-text"><i class="fas fa-clock"></i> <span class="info-box-text"><i class="fas fa-clock"></i>
${prettyDateTime(obj.Client.created_at)}</span> ${prettyDateTime(obj.Client.created_at)}</span>
@ -78,6 +79,34 @@ function renderClientList(data) {
}); });
} }
function renderUserList(data) {
$.each(data, function(index, obj) {
let clientStatusHtml = '>'
// render user html content
let html = `<div class="col-sm-6 col-md-6 col-lg-4" id="user_${obj.username}">
<div class="info-box">
<div class="info-box-content">
<div class="btn-group">
<button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" data-target="#modal_edit_user" data-username="${obj.username}">Edit</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal"
data-target="#modal_remove_user" data-username="${obj.username}">Delete</button>
</div>
<hr>
<span class="info-box-text"><i class="fas fa-user"></i> ${obj.username}</span>
<span class="info-box-text"><i class="fas fa-terminal"></i> ${obj.admin? 'Administrator':'Manager'}</span>
</div>
</div>
</div>`
// add the user html elements to the list
$('#users-list').append(html);
});
}
function prettyDateTime(timeStr) { function prettyDateTime(timeStr) {
const dt = new Date(timeStr); const dt = new Date(timeStr);
const offsetMs = dt.getTimezoneOffset() * 60 * 1000; const offsetMs = dt.getTimezoneOffset() * 60 * 1000;

View file

@ -0,0 +1,30 @@
## Prerequisites
### Kernel Module
Depending on if the Wireguard kernel module is available on your system you have more or less choices which example to use.
You can check if the kernel modules are available via the following command:
```shell
modprobe wireguard
```
If the command exits successfully and doesn't print an error the kernel modules are available.
If it does error, you either have to install them manually (or activate if deactivated) or use an userspace implementation.
For an example of an userspace implementation, see _borigtun_.
### Credentials
Username and password for all examples is `admin` by default.
For security reasons it's highly recommended to change them before the first startup.
## Examples
- **[system](system.yml)**
If you have Wireguard already installed on your system and only want to run the UI in docker this might fit the most.
- **[linuxserver](linuxserver.yml)**
If you have the Wireguard kernel modules installed (included in the mainline kernel since version 5.6) but want it running inside of docker, this might fit the most.
- **[boringtun](boringtun.yml)**
If Wireguard kernel modules are not available, you can switch to an userspace implementation like [boringtun](https://github.com/cloudflare/boringtun).

View file

@ -0,0 +1,43 @@
version: "3"
services:
boringtun:
image: ghcr.io/ntkme/boringtun:edge
command:
- wg0
container_name: boringtun
# use the network of the 'wireguard-ui' service. this enables to show active clients in the status page
network_mode: service:wireguard-ui
cap_add:
- NET_ADMIN
volumes:
- /dev/net/tun:/dev/net/tun
- ./config:/etc/wireguard
wireguard-ui:
image: ngoduykhanh/wireguard-ui:latest
container_name: wireguard-ui
cap_add:
- NET_ADMIN
environment:
- SENDGRID_API_KEY
- EMAIL_FROM_ADDRESS
- EMAIL_FROM_NAME
- SESSION_SECRET
- WGUI_USERNAME=admin
- WGUI_PASSWORD=admin
- WG_CONF_TEMPLATE
- WGUI_MANAGE_START=true
- WGUI_MANAGE_RESTART=true
logging:
driver: json-file
options:
max-size: 50m
volumes:
- ./db:/app/db
- ./config:/etc/wireguard
ports:
# port for wireguard-ui
- "5000:5000"
# port of the wireguard server. this must be set here as the `boringtun` container joins the network of this container and hasn't its own network over which it could publish the ports
- "51820:51820/udp"

View file

@ -0,0 +1,42 @@
version: "3"
services:
wireguard:
image: linuxserver/wireguard:latest
container_name: wireguard
cap_add:
- NET_ADMIN
volumes:
- ./config:/config
ports:
# port for wireguard-ui. this must be set here as the `wireguard-ui` container joins the network of this container and hasn't its own network over which it could publish the ports
- "5000:5000"
# port of the wireguard server
- "51820:51820/udp"
wireguard-ui:
image: ngoduykhanh/wireguard-ui:latest
container_name: wireguard-ui
depends_on:
- wireguard
cap_add:
- NET_ADMIN
# use the network of the 'wireguard' service. this enables to show active clients in the status page
network_mode: service:wireguard
environment:
- SENDGRID_API_KEY
- EMAIL_FROM_ADDRESS
- EMAIL_FROM_NAME
- SESSION_SECRET
- WGUI_USERNAME=admin
- WGUI_PASSWORD=admin
- WG_CONF_TEMPLATE
- WGUI_MANAGE_START=true
- WGUI_MANAGE_RESTART=true
logging:
driver: json-file
options:
max-size: 50m
volumes:
- ./db:/app/db
- ./config:/etc/wireguard

View file

@ -0,0 +1,27 @@
version: "3"
services:
wireguard-ui:
image: ngoduykhanh/wireguard-ui:latest
container_name: wireguard-ui
cap_add:
- NET_ADMIN
# required to show active clients. with this set, you don't need to expose the ui port (5000) anymore
network_mode: host
environment:
- SENDGRID_API_KEY
- EMAIL_FROM_ADDRESS
- EMAIL_FROM_NAME
- SESSION_SECRET
- WGUI_USERNAME=admin
- WGUI_PASSWORD=admin
- WG_CONF_TEMPLATE
- WGUI_MANAGE_START=false
- WGUI_MANAGE_RESTART=false
logging:
driver: json-file
options:
max-size: 50m
volumes:
- ./db:/app/db
- /etc/wireguard:/etc/wireguard

6
go.mod
View file

@ -1,9 +1,8 @@
module github.com/alikhanich/wireguard-ui module github.com/alikhanich/wireguard-ui
go 1.14 go 1.16
require ( require (
github.com/GeertJohan/go.rice v1.0.2
github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd
github.com/go-playground/universal-translator v0.17.0 // indirect github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/gorilla/sessions v1.2.0 github.com/gorilla/sessions v1.2.0
@ -19,7 +18,8 @@ require (
github.com/sendgrid/sendgrid-go v3.10.0+incompatible github.com/sendgrid/sendgrid-go v3.10.0+incompatible
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086
github.com/xhit/go-simple-mail/v2 v2.10.0 github.com/xhit/go-simple-mail/v2 v2.10.0
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/mod v0.7.0
//golang.zx2c4.com/wireguard v0.0.20200121 // indirect //golang.zx2c4.com/wireguard v0.0.20200121 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect

33
go.sum
View file

@ -1,8 +1,4 @@
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.2 h1:PtRw+Tg3oa3HYwiDBZyvOJ8LdIyf6lAovJJtr7YOAYk=
github.com/GeertJohan/go.rice v1.0.2/go.mod h1:af5vUNlDNkCjOZeSGFgIJxDje9qdjsO6hshx0gTmZt4=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
@ -12,8 +8,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/casbin/casbin/v2 v2.0.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/casbin/casbin/v2 v2.0.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/bbolt v1.3.1-coreos.6.0.20180223184059-4f5275f4ebbf/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.1-coreos.6.0.20180223184059-4f5275f4ebbf/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -52,7 +46,6 @@ github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8= github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8=
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw= github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw=
github.com/jessevdk/go-flags v0.0.0-20150816100521-1acbbaff2f34/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v0.0.0-20150816100521-1acbbaff2f34/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
@ -109,7 +102,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -157,6 +149,7 @@ github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/xhit/go-simple-mail/v2 v2.10.0 h1:nib6RaJ4qVh5HD9UE9QJqnUZyWp3upv+Z6CFxaMj0V8= github.com/xhit/go-simple-mail/v2 v2.10.0 h1:nib6RaJ4qVh5HD9UE9QJqnUZyWp3upv+Z6CFxaMj0V8=
github.com/xhit/go-simple-mail/v2 v2.10.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4= github.com/xhit/go-simple-mail/v2 v2.10.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -164,13 +157,18 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e h1:8foAy0aoO5GkqCvAEJ4VC4P3zksTg4X4aJCDpZzmgQI=
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -181,11 +179,13 @@ golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -215,19 +215,27 @@ golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -236,7 +244,6 @@ golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmo
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=

View file

@ -5,13 +5,13 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/fs"
"net/http" "net/http"
"os" "os"
"sort" "sort"
"strings" "strings"
"time" "time"
rice "github.com/GeertJohan/go.rice"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session" "github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -52,39 +52,54 @@ func LoginPage() echo.HandlerFunc {
// Login for signing in handler // Login for signing in handler
func Login(db store.IStore) echo.HandlerFunc { func Login(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
user := new(model.User) data := make(map[string]interface{})
c.Bind(user) err := json.NewDecoder(c.Request().Body).Decode(&data)
dbuser, err := db.GetUser() if err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
}
username := data["username"].(string)
password := data["password"].(string)
rememberMe := data["rememberMe"].(bool)
dbuser, err := db.GetUserByName(username)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot query user from DB"}) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot query user from DB"})
} }
userCorrect := subtle.ConstantTimeCompare([]byte(user.Username), []byte(dbuser.Username)) == 1 userCorrect := subtle.ConstantTimeCompare([]byte(username), []byte(dbuser.Username)) == 1
var passwordCorrect bool var passwordCorrect bool
if dbuser.PasswordHash != "" { if dbuser.PasswordHash != "" {
match, err := util.VerifyHash(dbuser.PasswordHash, user.Password) match, err := util.VerifyHash(dbuser.PasswordHash, password)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify password"}) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify password"})
} }
passwordCorrect = match passwordCorrect = match
} else { } else {
passwordCorrect = subtle.ConstantTimeCompare([]byte(user.Password), []byte(dbuser.Password)) == 1 passwordCorrect = subtle.ConstantTimeCompare([]byte(password), []byte(dbuser.Password)) == 1
} }
if userCorrect && passwordCorrect { if userCorrect && passwordCorrect {
// TODO: refresh the token // TODO: refresh the token
ageMax := 0
expiration := time.Now().Add(24 * time.Hour)
if rememberMe {
ageMax = 86400
expiration.Add(144 * time.Hour)
}
sess, _ := session.Get("session", c) sess, _ := session.Get("session", c)
sess.Options = &sessions.Options{ sess.Options = &sessions.Options{
Path: util.BasePath, Path: util.BasePath,
MaxAge: 86400, MaxAge: ageMax,
HttpOnly: true, HttpOnly: true,
} }
// set session_token // set session_token
tokenUID := xid.New().String() tokenUID := xid.New().String()
sess.Values["username"] = user.Username sess.Values["username"] = dbuser.Username
sess.Values["admin"] = dbuser.Admin
sess.Values["session_token"] = tokenUID sess.Values["session_token"] = tokenUID
sess.Save(c.Request(), c.Response()) sess.Save(c.Request(), c.Response())
@ -92,7 +107,7 @@ func Login(db store.IStore) echo.HandlerFunc {
cookie := new(http.Cookie) cookie := new(http.Cookie)
cookie.Name = "session_token" cookie.Name = "session_token"
cookie.Value = tokenUID cookie.Value = tokenUID
cookie.Expires = time.Now().Add(24 * time.Hour) cookie.Expires = expiration
c.SetCookie(cookie) c.SetCookie(cookie)
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Logged in successfully"}) return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Logged in successfully"})
@ -102,6 +117,40 @@ func Login(db store.IStore) echo.HandlerFunc {
} }
} }
// GetUsers handler return a JSON list of all users
func GetUsers(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
usersList, err := db.GetUsers()
if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
false, fmt.Sprintf("Cannot get user list: %v", err),
})
}
return c.JSON(http.StatusOK, usersList)
}
}
// GetUser handler returns a JSON object of single user
func GetUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
username := c.Param("username")
if !isAdmin(c) && (username != currentUser(c)) {
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"})
}
userData, err := db.GetUserByName(username)
if err != nil {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "User not found"})
}
return c.JSON(http.StatusOK, userData)
}
}
// Logout to log a user out // Logout to log a user out
func Logout() echo.HandlerFunc { func Logout() echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -113,21 +162,23 @@ func Logout() echo.HandlerFunc {
// LoadProfile to load user information // LoadProfile to load user information
func LoadProfile(db store.IStore) echo.HandlerFunc { func LoadProfile(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
userInfo, err := db.GetUser()
if err != nil {
log.Error("Cannot get user information: ", err)
}
return c.Render(http.StatusOK, "profile.html", map[string]interface{}{ return c.Render(http.StatusOK, "profile.html", map[string]interface{}{
"baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c)}, "baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"userInfo": userInfo,
}) })
} }
} }
// UpdateProfile to update user information // UsersSettings handler
func UpdateProfile(db store.IStore) echo.HandlerFunc { func UsersSettings(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
return c.Render(http.StatusOK, "users_settings.html", map[string]interface{}{
"baseData": model.BaseData{Active: "users-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)},
})
}
}
// UpdateUser to update user information
func UpdateUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
data := make(map[string]interface{}) data := make(map[string]interface{})
err := json.NewDecoder(c.Request().Body).Decode(&data) err := json.NewDecoder(c.Request().Body).Decode(&data)
@ -138,8 +189,18 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc {
username := data["username"].(string) username := data["username"].(string)
password := data["password"].(string) password := data["password"].(string)
previousUsername := data["previous_username"].(string)
admin := data["admin"].(bool)
user, err := db.GetUser() if !isAdmin(c) && (previousUsername != currentUser(c)) {
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"})
}
if !isAdmin(c) {
admin = false
}
user, err := db.GetUserByName(previousUsername)
if err != nil { if err != nil {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()})
} }
@ -150,6 +211,13 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc {
user.Username = username user.Username = username
} }
if username != previousUsername {
_, err := db.GetUserByName(username)
if err == nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "This username is taken"})
}
}
if password != "" { if password != "" {
hash, err := util.HashPassword(password) hash, err := util.HashPassword(password)
if err != nil { if err != nil {
@ -158,12 +226,96 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc {
user.PasswordHash = hash user.PasswordHash = hash
} }
if previousUsername != currentUser(c) {
user.Admin = admin
}
if err := db.DeleteUser(previousUsername); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
if err := db.SaveUser(user); err != nil { if err := db.SaveUser(user); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
} }
log.Infof("Updated admin user information successfully") log.Infof("Updated user information successfully")
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated admin user information successfully"}) if previousUsername == currentUser(c) {
setUser(c, user.Username, user.Admin)
}
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated user information successfully"})
}
}
// CreateUser to create new user
func CreateUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
data := make(map[string]interface{})
err := json.NewDecoder(c.Request().Body).Decode(&data)
if err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
}
var user model.User
username := data["username"].(string)
password := data["password"].(string)
admin := data["admin"].(bool)
if username == "" {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"})
} else {
user.Username = username
}
{
_, err := db.GetUserByName(username)
if err == nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "This username is taken"})
}
}
hash, err := util.HashPassword(password)
if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
user.PasswordHash = hash
user.Admin = admin
if err := db.SaveUser(user); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
log.Infof("Created user successfully")
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Created user successfully"})
}
}
// RemoveUser handler
func RemoveUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
data := make(map[string]interface{})
err := json.NewDecoder(c.Request().Body).Decode(&data)
if err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
}
username := data["username"].(string)
if username == currentUser(c) {
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "User cannot delete itself"})
}
// delete user from database
if err := db.DeleteUser(username); err != nil {
log.Error("Cannot delete user: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot delete user from database"})
}
log.Infof("Removed user: %s", username)
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "User removed"})
} }
} }
@ -179,7 +331,7 @@ func WireGuardClients(db store.IStore) echo.HandlerFunc {
} }
return c.Render(http.StatusOK, "clients.html", map[string]interface{}{ return c.Render(http.StatusOK, "clients.html", map[string]interface{}{
"baseData": model.BaseData{Active: "", CurrentUser: currentUser(c)}, "baseData": model.BaseData{Active: "", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"clientDataList": clientDataList, "clientDataList": clientDataList,
}) })
} }
@ -532,7 +684,7 @@ func WireGuardServer(db store.IStore) echo.HandlerFunc {
} }
return c.Render(http.StatusOK, "server.html", map[string]interface{}{ return c.Render(http.StatusOK, "server.html", map[string]interface{}{
"baseData": model.BaseData{Active: "wg-server", CurrentUser: currentUser(c)}, "baseData": model.BaseData{Active: "wg-server", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"serverInterface": server.Interface, "serverInterface": server.Interface,
"serverKeyPair": server.KeyPair, "serverKeyPair": server.KeyPair,
}) })
@ -600,7 +752,7 @@ func GlobalSettings(db store.IStore) echo.HandlerFunc {
} }
return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{ return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{
"baseData": model.BaseData{Active: "global-settings", CurrentUser: currentUser(c)}, "baseData": model.BaseData{Active: "global-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"globalSettings": globalSettings, "globalSettings": globalSettings,
}) })
} }
@ -630,7 +782,7 @@ func Status(db store.IStore) echo.HandlerFunc {
wgClient, err := wgctrl.New() wgClient, err := wgctrl.New()
if err != nil { if err != nil {
return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, "baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"error": err.Error(), "error": err.Error(),
"devices": nil, "devices": nil,
}) })
@ -639,7 +791,7 @@ func Status(db store.IStore) echo.HandlerFunc {
devices, err := wgClient.Devices() devices, err := wgClient.Devices()
if err != nil { if err != nil {
return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, "baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"error": err.Error(), "error": err.Error(),
"devices": nil, "devices": nil,
}) })
@ -651,7 +803,7 @@ func Status(db store.IStore) echo.HandlerFunc {
clients, err := db.GetClients(false) clients, err := db.GetClients(false)
if err != nil { if err != nil {
return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, "baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"error": err.Error(), "error": err.Error(),
"devices": nil, "devices": nil,
}) })
@ -697,7 +849,7 @@ func Status(db store.IStore) echo.HandlerFunc {
} }
return c.Render(http.StatusOK, "status.html", map[string]interface{}{ return c.Render(http.StatusOK, "status.html", map[string]interface{}{
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, "baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"devices": devicesVm, "devices": devicesVm,
"error": "", "error": "",
}) })
@ -796,7 +948,7 @@ func SuggestIPAllocation(db store.IStore) echo.HandlerFunc {
} }
// ApplyServerConfig handler to write config file and restart Wireguard server // ApplyServerConfig handler to write config file and restart Wireguard server
func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc { func ApplyServerConfig(db store.IStore, tmplDir fs.FS) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
server, err := db.GetServer() server, err := db.GetServer()
@ -811,6 +963,12 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"}) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"})
} }
users, err := db.GetUsers()
if err != nil {
log.Error("Cannot get users config: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get users config"})
}
settings, err := db.GetGlobalSettings() settings, err := db.GetGlobalSettings()
if err != nil { if err != nil {
log.Error("Cannot get global settings: ", err) log.Error("Cannot get global settings: ", err)
@ -818,7 +976,7 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc {
} }
// Write config file // Write config file
err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings) err = util.WriteWireGuardServerConfig(tmplDir, server, clients, users, settings)
if err != nil { if err != nil {
log.Error("Cannot apply server config: ", err) log.Error("Cannot apply server config: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
@ -826,16 +984,35 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc {
}) })
} }
err = util.UpdateHashes(db)
if err != nil {
log.Error("Cannot update hashes: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
false, fmt.Sprintf("Cannot update hashes: %v", err),
})
}
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Applied server config successfully"}) return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Applied server config successfully"})
} }
} }
// GetHashesChanges handler returns if database hashes have changed
func GetHashesChanges(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
if util.HashesChanged(db) {
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Hashes changed"})
} else {
return c.JSON(http.StatusOK, jsonHTTPResponse{false, "Hashes not changed"})
}
}
}
// AboutPage handler // AboutPage handler
func AboutPage() echo.HandlerFunc { func AboutPage() echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
return c.Render(http.StatusOK, "about.html", map[string]interface{}{ return c.Render(http.StatusOK, "about.html", map[string]interface{}{
"baseData": model.BaseData{Active: "about", CurrentUser: currentUser(c)}, "baseData": model.BaseData{Active: "about", CurrentUser: currentUser(c), Admin: isAdmin(c)},
}) })
} }
} }

View file

@ -37,7 +37,7 @@ func GetWakeOnLanHosts(db store.IStore) echo.HandlerFunc {
} }
err = c.Render(http.StatusOK, "wake_on_lan_hosts.html", map[string]interface{}{ err = c.Render(http.StatusOK, "wake_on_lan_hosts.html", map[string]interface{}{
"baseData": model.BaseData{Active: "wake_on_lan_hosts", CurrentUser: currentUser(c)}, "baseData": model.BaseData{Active: "wake_on_lan_hosts", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"hosts": hosts, "hosts": hosts,
"error": "", "error": "",
}) })

View file

@ -14,15 +14,24 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc {
if !isValidSession(c) { if !isValidSession(c) {
nextURL := c.Request().URL nextURL := c.Request().URL
if nextURL != nil && c.Request().Method == http.MethodGet { if nextURL != nil && c.Request().Method == http.MethodGet {
return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath + "/login?next=%s", c.Request().URL)) return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath+"/login?next=%s", c.Request().URL))
} else { } else {
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath + "/login") return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login")
} }
} }
return next(c) return next(c)
} }
} }
func NeedsAdmin(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if !isAdmin(c) {
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/")
}
return next(c)
}
}
func isValidSession(c echo.Context) bool { func isValidSession(c echo.Context) bool {
if util.DisableLogin { if util.DisableLogin {
return true return true
@ -50,10 +59,29 @@ func currentUser(c echo.Context) string {
return username return username
} }
// isAdmin to get user type: admin or manager
func isAdmin(c echo.Context) bool {
if util.DisableLogin {
return true
}
sess, _ := session.Get("session", c)
admin := fmt.Sprintf("%t", sess.Values["admin"])
return admin == "true"
}
func setUser(c echo.Context, username string, admin bool) {
sess, _ := session.Get("session", c)
sess.Values["username"] = username
sess.Values["admin"] = admin
sess.Save(c.Request(), c.Response())
}
// clearSession to remove current session // clearSession to remove current session
func clearSession(c echo.Context) { func clearSession(c echo.Context) {
sess, _ := session.Get("session", c) sess, _ := session.Get("session", c)
sess.Values["username"] = "" sess.Values["username"] = ""
sess.Values["admin"] = false
sess.Values["session_token"] = "" sess.Values["session_token"] = ""
sess.Save(c.Request(), c.Response()) sess.Save(c.Request(), c.Response())
} }

63
main.go
View file

@ -1,15 +1,16 @@
package main package main
import ( import (
"embed"
"flag" "flag"
"fmt" "fmt"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
"github.com/alikhanich/wireguard-ui/store" "github.com/ngoduykhanh/wireguard-ui/store"
"io/fs"
"net/http" "net/http"
"os" "os"
"time" "time"
rice "github.com/GeertJohan/go.rice" rice "github.com/GeertJohan/go.rice"
"github.com/alikhanich/wireguard-ui/emailer" "github.com/alikhanich/wireguard-ui/emailer"
"github.com/alikhanich/wireguard-ui/handler" "github.com/alikhanich/wireguard-ui/handler"
@ -52,6 +53,16 @@ const (
` `
) )
// embed the "templates" directory
//
//go:embed templates/*
var embeddedTemplates embed.FS
// embed the "assets" directory
//
//go:embed assets/*
var embeddedAssets embed.FS
func init() { func init() {
// command-line flags and env variables // command-line flags and env variables
@ -119,19 +130,18 @@ func main() {
// set app extra data // set app extra data
extraData := make(map[string]string) extraData := make(map[string]string)
extraData["appVersion"] = appVersion extraData["appVersion"] = appVersion
extraData["gitCommit"] = gitCommit
extraData["basePath"] = util.BasePath extraData["basePath"] = util.BasePath
// create rice box for embedded template // strip the "templates/" prefix from the embedded directory so files can be read by their direct name (e.g.
tmplBox := rice.MustFindBox("templates") // "base.html" instead of "templates/base.html")
tmplDir, _ := fs.Sub(fs.FS(embeddedTemplates), "templates")
// rice file server for assets. "assets" is the folder where the files come from.
assetHandler := http.FileServer(rice.MustFindBox("assets").HTTPBox())
// create the wireguard config on start, if it doesn't exist // create the wireguard config on start, if it doesn't exist
initServerConfig(db, tmplBox) initServerConfig(db, tmplDir)
// register routes // register routes
app := router.New(tmplBox, extraData, util.SessionSecret) app := router.New(tmplDir, extraData, util.SessionSecret)
app.GET(util.BasePath, handler.WireGuardClients(db), handler.ValidSession) app.GET(util.BasePath, handler.WireGuardClients(db), handler.ValidSession)
@ -140,7 +150,12 @@ func main() {
app.POST(util.BasePath+"/login", handler.Login(db)) app.POST(util.BasePath+"/login", handler.Login(db))
app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession) app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession)
app.GET(util.BasePath+"/profile", handler.LoadProfile(db), handler.ValidSession) app.GET(util.BasePath+"/profile", handler.LoadProfile(db), handler.ValidSession)
app.POST(util.BasePath+"/profile", handler.UpdateProfile(db), handler.ValidSession) app.GET(util.BasePath+"/users-settings", handler.UsersSettings(db), handler.ValidSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/update-user", handler.UpdateUser(db), handler.ValidSession)
app.POST(util.BasePath+"/create-user", handler.CreateUser(db), handler.ValidSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/remove-user", handler.RemoveUser(db), handler.ValidSession, handler.NeedsAdmin)
app.GET(util.BasePath+"/getusers", handler.GetUsers(db), handler.ValidSession, handler.NeedsAdmin)
app.GET(util.BasePath+"/api/user/:username", handler.GetUser(db), handler.ValidSession)
} }
var sendmail emailer.Emailer var sendmail emailer.Emailer
@ -150,6 +165,7 @@ func main() {
sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption) sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption)
} }
app.GET(util.BasePath+"/test-hash", handler.GetHashesChanges(db), handler.ValidSession)
app.GET(util.BasePath+"/about", handler.AboutPage()) app.GET(util.BasePath+"/about", handler.AboutPage())
app.GET(util.BasePath+"/_health", handler.Health()) app.GET(util.BasePath+"/_health", handler.Health())
app.GET(util.BasePath+"/favicon", handler.Favicon()) app.GET(util.BasePath+"/favicon", handler.Favicon())
@ -159,29 +175,33 @@ func main() {
app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson)
app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession) app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession)
app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession) app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession) app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession) app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession)
app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession) app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession)
app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession) app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession)
app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession) app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession)
app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession) app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession)
app.POST(util.BasePath+"/api/apply-wg-config", handler.ApplyServerConfig(db, tmplBox), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/api/apply-wg-config", handler.ApplyServerConfig(db, tmplDir), handler.ValidSession, handler.ContentTypeJson)
app.GET(util.BasePath+"/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession) app.GET(util.BasePath+"/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession)
app.POST(util.BasePath+"/wake_on_lan_host", handler.SaveWakeOnLanHost(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/wake_on_lan_host", handler.SaveWakeOnLanHost(db), handler.ValidSession, handler.ContentTypeJson)
app.DELETE(util.BasePath+"/wake_on_lan_host/:mac_address", handler.DeleteWakeOnHost(db), handler.ValidSession, handler.ContentTypeJson) app.DELETE(util.BasePath+"/wake_on_lan_host/:mac_address", handler.DeleteWakeOnHost(db), handler.ValidSession, handler.ContentTypeJson)
app.PUT(util.BasePath+"/wake_on_lan_host/:mac_address", handler.WakeOnHost(db), handler.ValidSession, handler.ContentTypeJson) app.PUT(util.BasePath+"/wake_on_lan_host/:mac_address", handler.WakeOnHost(db), handler.ValidSession, handler.ContentTypeJson)
// servers other static files // strip the "assets/" prefix from the embedded directory so files can be called directly without the "assets/"
// prefix
assetsDir, _ := fs.Sub(fs.FS(embeddedAssets), "assets")
assetHandler := http.FileServer(http.FS(assetsDir))
// serves other static files
app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler))) app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler)))
app.Logger.Fatal(app.Start(util.BindAddress)) app.Logger.Fatal(app.Start(util.BindAddress))
} }
func initServerConfig(db store.IStore, tmplBox *rice.Box) { func initServerConfig(db store.IStore, tmplDir fs.FS) {
settings, err := db.GetGlobalSettings() settings, err := db.GetGlobalSettings()
if err != nil { if err != nil {
log.Fatalf("Cannot get global settings: ", err) log.Fatalf("Cannot get global settings: ", err)
@ -202,8 +222,13 @@ func initServerConfig(db store.IStore, tmplBox *rice.Box) {
log.Fatalf("Cannot get client config: ", err) log.Fatalf("Cannot get client config: ", err)
} }
users, err := db.GetUsers()
if err != nil {
log.Fatalf("Cannot get user config: ", err)
}
// write config file // write config file
err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings) err = util.WriteWireGuardServerConfig(tmplDir, server, clients, users, settings)
if err != nil { if err != nil {
log.Fatalf("Cannot create server config: ", err) log.Fatalf("Cannot create server config: ", err)
} }

View file

@ -10,4 +10,11 @@ type Interface struct {
type BaseData struct { type BaseData struct {
Active string Active string
CurrentUser string CurrentUser string
Admin bool
}
// ClientServerHashes struct, to save hashes to detect changes
type ClientServerHashes struct {
Client string `json:"client"`
Server string `json:"server"`
} }

View file

@ -10,7 +10,7 @@ type GlobalSetting struct {
DNSServers []string `json:"dns_servers"` DNSServers []string `json:"dns_servers"`
MTU int `json:"mtu,string"` MTU int `json:"mtu,string"`
PersistentKeepalive int `json:"persistent_keepalive,string"` PersistentKeepalive int `json:"persistent_keepalive,string"`
ForwardMark string `json:"forward_mark"` FirewallMark string `json:"firewall_mark"`
ConfigFilePath string `json:"config_file_path"` ConfigFilePath string `json:"config_file_path"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }

View file

@ -6,4 +6,5 @@ type User struct {
Password string `json:"password"` Password string `json:"password"`
// PasswordHash takes precedence over Password. // PasswordHash takes precedence over Password.
PasswordHash string `json:"password_hash"` PasswordHash string `json:"password_hash"`
Admin bool `json:"admin"`
} }

View file

@ -3,11 +3,11 @@ package router
import ( import (
"errors" "errors"
"io" "io"
"io/fs"
"reflect" "reflect"
"strings" "strings"
"text/template" "text/template"
rice "github.com/GeertJohan/go.rice"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session" "github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -66,54 +66,59 @@ func apiKeyMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
} }
} }
// New function // New function
func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Echo { func New(tmplDir fs.FS, extraData map[string]string, secret []byte) *echo.Echo {
e := echo.New() e := echo.New()
store := sessions.NewCookieStore(secret) store := sessions.NewCookieStore(secret)
e.Use(session.Middleware(store)) e.Use(session.Middleware(store))
e.Use(apiKeyMiddleware) e.Use(apiKeyMiddleware)
// read html template file to string // read html template file to string
tmplBaseString, err := tmplBox.String("base.html") tmplBaseString, err := util.StringFromEmbedFile(tmplDir, "base.html")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
tmplLoginString, err := tmplBox.String("login.html") tmplLoginString, err := util.StringFromEmbedFile(tmplDir, "login.html")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
tmplProfileString, err := tmplBox.String("profile.html") tmplProfileString, err := util.StringFromEmbedFile(tmplDir, "profile.html")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
tmplClientsString, err := tmplBox.String("clients.html") tmplClientsString, err := util.StringFromEmbedFile(tmplDir, "clients.html")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
tmplServerString, err := tmplBox.String("server.html") tmplServerString, err := util.StringFromEmbedFile(tmplDir, "server.html")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
tmplGlobalSettingsString, err := tmplBox.String("global_settings.html") tmplGlobalSettingsString, err := util.StringFromEmbedFile(tmplDir, "global_settings.html")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
tmplStatusString, err := tmplBox.String("status.html") tmplUsersSettingsString, err := util.StringFromEmbedFile(tmplDir, "users_settings.html")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
tmplWakeOnLanHostsString, err := tmplBox.String("wake_on_lan_hosts.html") tmplStatusString, err := util.StringFromEmbedFile(tmplDir, "status.html")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
aboutPageString, err := tmplBox.String("about.html") tmplWakeOnLanHostsString, err := util.StringFromEmbedFile(tmplDir, "wake_on_lan_hosts.html")
if err != nil {
log.Fatal(err)
}
aboutPageString, err := util.StringFromEmbedFile(tmplDir, "about.html")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -128,14 +133,33 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec
templates["clients.html"] = template.Must(template.New("clients").Funcs(funcs).Parse(tmplBaseString + tmplClientsString)) templates["clients.html"] = template.Must(template.New("clients").Funcs(funcs).Parse(tmplBaseString + tmplClientsString))
templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString)) templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString))
templates["global_settings.html"] = template.Must(template.New("global_settings").Funcs(funcs).Parse(tmplBaseString + tmplGlobalSettingsString)) templates["global_settings.html"] = template.Must(template.New("global_settings").Funcs(funcs).Parse(tmplBaseString + tmplGlobalSettingsString))
templates["users_settings.html"] = template.Must(template.New("users_settings").Funcs(funcs).Parse(tmplBaseString + tmplUsersSettingsString))
templates["status.html"] = template.Must(template.New("status").Funcs(funcs).Parse(tmplBaseString + tmplStatusString)) templates["status.html"] = template.Must(template.New("status").Funcs(funcs).Parse(tmplBaseString + tmplStatusString))
templates["wake_on_lan_hosts.html"] = template.Must(template.New("wake_on_lan_hosts").Funcs(funcs).Parse(tmplBaseString + tmplWakeOnLanHostsString)) templates["wake_on_lan_hosts.html"] = template.Must(template.New("wake_on_lan_hosts").Funcs(funcs).Parse(tmplBaseString + tmplWakeOnLanHostsString))
templates["about.html"] = template.Must(template.New("about").Funcs(funcs).Parse(tmplBaseString + aboutPageString)) templates["about.html"] = template.Must(template.New("about").Funcs(funcs).Parse(tmplBaseString + aboutPageString))
e.Logger.SetLevel(log.DEBUG) lvl, err := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO"))
if err != nil {
log.Fatal(err)
}
logConfig := middleware.DefaultLoggerConfig
logConfig.Skipper = func(c echo.Context) bool {
resp := c.Response()
if resp.Status >= 500 && lvl > log.ERROR { // do not log if response is 5XX but log level is higher than ERROR
return true
} else if resp.Status >= 400 && lvl > log.WARN { // do not log if response is 4XX but log level is higher than WARN
return true
} else if lvl > log.DEBUG { // do not log if log level is higher than DEBUG
return true
}
return false
}
e.Logger.SetLevel(lvl)
e.Pre(middleware.RemoveTrailingSlash()) e.Pre(middleware.RemoveTrailingSlash())
e.Use(middleware.Logger()) e.Use(middleware.LoggerWithConfig(logConfig))
e.HideBanner = true e.HideBanner = true
e.HidePort = lvl > log.INFO // hide the port output if the log level is higher than INFO
e.Validator = NewValidator() e.Validator = NewValidator()
e.Renderer = &TemplateRegistry{ e.Renderer = &TemplateRegistry{
templates: templates, templates: templates,

View file

@ -42,7 +42,9 @@ func (o *JsonDB) Init() error {
var serverInterfacePath string = path.Join(serverPath, "interfaces.json") var serverInterfacePath string = path.Join(serverPath, "interfaces.json")
var serverKeyPairPath string = path.Join(serverPath, "keypair.json") var serverKeyPairPath string = path.Join(serverPath, "keypair.json")
var globalSettingPath string = path.Join(serverPath, "global_settings.json") var globalSettingPath string = path.Join(serverPath, "global_settings.json")
var hashesPath string = path.Join(serverPath, "hashes.json")
var userPath string = path.Join(serverPath, "users.json") var userPath string = path.Join(serverPath, "users.json")
// create directories if they do not exist // create directories if they do not exist
if _, err := os.Stat(clientPath); os.IsNotExist(err) { if _, err := os.Stat(clientPath); os.IsNotExist(err) {
os.MkdirAll(clientPath, os.ModePerm) os.MkdirAll(clientPath, os.ModePerm)
@ -53,6 +55,9 @@ func (o *JsonDB) Init() error {
if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) { if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) {
os.MkdirAll(wakeOnLanHostsPath, os.ModePerm) os.MkdirAll(wakeOnLanHostsPath, os.ModePerm)
} }
if _, err := os.Stat(userPath); os.IsNotExist(err) {
os.MkdirAll(userPath, os.ModePerm)
}
// server's interface // server's interface
if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) { if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) {
@ -96,16 +101,26 @@ func (o *JsonDB) Init() error {
globalSetting.DNSServers = util.LookupEnvOrStrings(util.DNSEnvVar, []string{util.DefaultDNS}) globalSetting.DNSServers = util.LookupEnvOrStrings(util.DNSEnvVar, []string{util.DefaultDNS})
globalSetting.MTU = util.LookupEnvOrInt(util.MTUEnvVar, util.DefaultMTU) globalSetting.MTU = util.LookupEnvOrInt(util.MTUEnvVar, util.DefaultMTU)
globalSetting.PersistentKeepalive = util.LookupEnvOrInt(util.PersistentKeepaliveEnvVar, util.DefaultPersistentKeepalive) globalSetting.PersistentKeepalive = util.LookupEnvOrInt(util.PersistentKeepaliveEnvVar, util.DefaultPersistentKeepalive)
globalSetting.ForwardMark = util.LookupEnvOrString(util.ForwardMarkEnvVar, util.DefaultForwardMark) globalSetting.FirewallMark = util.LookupEnvOrString(util.FirewallMarkEnvVar, util.DefaultFirewallMark)
globalSetting.ConfigFilePath = util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath) globalSetting.ConfigFilePath = util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath)
globalSetting.UpdatedAt = time.Now().UTC() globalSetting.UpdatedAt = time.Now().UTC()
o.conn.Write("server", "global_settings", globalSetting) o.conn.Write("server", "global_settings", globalSetting)
} }
// hashes
if _, err := os.Stat(hashesPath); os.IsNotExist(err) {
clientServerHashes := new(model.ClientServerHashes)
clientServerHashes.Client = "none"
clientServerHashes.Server = "none"
o.conn.Write("server", "hashes", clientServerHashes)
}
// user info // user info
if _, err := os.Stat(userPath); os.IsNotExist(err) { results, err := o.conn.ReadAll("users")
if err != nil || len(results) < 1 {
user := new(model.User) user := new(model.User)
user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername) user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername)
user.Admin = util.DefaultIsAdmin
user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "") user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "")
if user.PasswordHash == "" { if user.PasswordHash == "" {
plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword) plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword)
@ -115,7 +130,7 @@ func (o *JsonDB) Init() error {
} }
user.PasswordHash = hash user.PasswordHash = hash
} }
o.conn.Write("server", "users", user) o.conn.Write("users", user.Username, user)
} }
return nil return nil
@ -127,9 +142,44 @@ func (o *JsonDB) GetUser() (model.User, error) {
return user, o.conn.Read("server", "users", &user) return user, o.conn.Read("server", "users", &user)
} }
// SaveUser func to user info to the database // GetUsers func to get all users from the database
func (o *JsonDB) GetUsers() ([]model.User, error) {
var users []model.User
results, err := o.conn.ReadAll("users")
if err != nil {
return users, err
}
for _, i := range results {
user := model.User{}
if err := json.Unmarshal([]byte(i), &user); err != nil {
return users, fmt.Errorf("cannot decode user json structure: %v", err)
}
users = append(users, user)
}
return users, err
}
// GetUserByName func to get single user from the database
func (o *JsonDB) GetUserByName(username string) (model.User, error) {
user := model.User{}
if err := o.conn.Read("users", username, &user); err != nil {
return user, err
}
return user, nil
}
// SaveUser func to save user in the database
func (o *JsonDB) SaveUser(user model.User) error { func (o *JsonDB) SaveUser(user model.User) error {
return o.conn.Write("server", "users", user) return o.conn.Write("users", user.Username, user)
}
// DeleteUser func to remove user from the database
func (o *JsonDB) DeleteUser(username string) error {
return o.conn.Delete("users", username)
} }
// GetGlobalSettings func to query global settings from the database // GetGlobalSettings func to query global settings from the database
@ -213,15 +263,12 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti
server, _ := o.GetServer() server, _ := o.GetServer()
globalSettings, _ := o.GetGlobalSettings() globalSettings, _ := o.GetGlobalSettings()
client := client client := client
if !qrCodeSettings.IncludeDNS{ if !qrCodeSettings.IncludeDNS {
globalSettings.DNSServers = []string{} globalSettings.DNSServers = []string{}
} }
if !qrCodeSettings.IncludeMTU { if !qrCodeSettings.IncludeMTU {
globalSettings.MTU = 0 globalSettings.MTU = 0
} }
if !qrCodeSettings.IncludeFwMark {
globalSettings.ForwardMark = ""
}
png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256) png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256)
if err == nil { if err == nil {
@ -255,3 +302,16 @@ func (o *JsonDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error {
func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error { func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error {
return o.conn.Write("server", "global_settings", globalSettings) return o.conn.Write("server", "global_settings", globalSettings)
} }
func (o *JsonDB) GetPath() string {
return o.dbPath
}
func (o *JsonDB) GetHashes() (model.ClientServerHashes, error) {
hashes := model.ClientServerHashes{}
return hashes, o.conn.Read("server", "hashes", &hashes)
}
func (o *JsonDB) SaveHashes(hashes model.ClientServerHashes) error {
return o.conn.Write("server", "hashes", hashes)
}

View file

@ -6,8 +6,10 @@ import (
type IStore interface { type IStore interface {
Init() error Init() error
GetUser() (model.User, error) GetUsers() ([]model.User, error)
GetUserByName(username string) (model.User, error)
SaveUser(user model.User) error SaveUser(user model.User) error
DeleteUser(username string) error
GetGlobalSettings() (model.GlobalSetting, error) GetGlobalSettings() (model.GlobalSetting, error)
GetServer() (model.Server, error) GetServer() (model.Server, error)
GetClients(hasQRCode bool) ([]model.ClientData, error) GetClients(hasQRCode bool) ([]model.ClientData, error)
@ -22,4 +24,7 @@ type IStore interface {
DeleteWakeOnHostLanHost(macAddress string) error DeleteWakeOnHostLanHost(macAddress string) error
SaveWakeOnLanHost(host model.WakeOnLanHost) error SaveWakeOnLanHost(host model.WakeOnLanHost) error
DeleteWakeOnHost(host model.WakeOnLanHost) error DeleteWakeOnHost(host model.WakeOnLanHost) error
GetPath() string
SaveHashes(hashes model.ClientServerHashes) error
GetHashes() (model.ClientServerHashes, error)
} }

View file

@ -30,6 +30,12 @@ About
<label for="version" class="control-label">Current version</label> <label for="version" class="control-label">Current version</label>
<input type="text" class="form-control" id="version" value="{{ .appVersion }}" readonly> <input type="text" class="form-control" id="version" value="{{ .appVersion }}" readonly>
</div> </div>
{{ if .gitCommit }}
<div class="form-group">
<label for="version" class="control-label">git commit hash</label>
<input type="text" class="form-control" id="version" value="{{ .gitCommit }}" readonly>
</div>
{{ end }}
<div class="form-group"> <div class="form-group">
<label for="currentReleaseDate" class="control-label">Current version release date</label> <label for="currentReleaseDate" class="control-label">Current version release date</label>
<input type="text" class="form-control" id="currentReleaseDate" readonly> <input type="text" class="form-control" id="currentReleaseDate" readonly>

View file

@ -56,6 +56,15 @@
</button> </button>
</div> </div>
</div> </div>
<div class="form-group form-group-sm">
<select name="status-selector" id="status-selector" class="custom-select form-control-navbar" style="margin-left: 0.5em; height: 90%; font-size: 14px;">
<option value="All">All</option>
<option value="Enabled">Enabled</option>
<option value="Disabled">Disabled</option>
<option value="Connected">Connected</option>
<option value="Disconnected">Disconnected</option>
</select>
</div>
</form> </form>
<!-- Right navbar links --> <!-- Right navbar links -->
@ -63,7 +72,7 @@
<button style="margin-left: 0.5em;" type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" <button style="margin-left: 0.5em;" type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal"
data-target="#modal_new_client"><i class="nav-icon fas fa-plus"></i> New data-target="#modal_new_client"><i class="nav-icon fas fa-plus"></i> New
Client</button> Client</button>
<button style="margin-left: 0.5em;" type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal" <button id="apply-config-button" style="margin-left: 0.5em; display: none;" type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal"
data-target="#modal_apply_config"><i class="nav-icon fas fa-check"></i> Apply data-target="#modal_apply_config"><i class="nav-icon fas fa-check"></i> Apply
Config</button> Config</button>
{{if .baseData.CurrentUser}} {{if .baseData.CurrentUser}}
@ -90,7 +99,13 @@
</div> </div>
<div class="info"> <div class="info">
{{if .baseData.CurrentUser}} {{if .baseData.CurrentUser}}
<a href="{{.basePath}}/profile" class="d-block">{{.baseData.CurrentUser}}</a>
{{if .baseData.Admin}}
<a href="{{.basePath}}/profile" class="d-block">Administrator: {{.baseData.CurrentUser}}</a>
{{else}}
<a href="{{.basePath}}/profile" class="d-block">Manager: {{.baseData.CurrentUser}}</a>
{{end}}
{{else}} {{else}}
<a href="#" class="d-block">Administrator</a> <a href="#" class="d-block">Administrator</a>
{{end}} {{end}}
@ -109,6 +124,8 @@
</p> </p>
</a> </a>
</li> </li>
{{if .baseData.Admin}}
<li class="nav-item"> <li class="nav-item">
<a href="{{.basePath}}/wg-server" class="nav-link {{if eq .baseData.Active "wg-server" }}active{{end}}"> <a href="{{.basePath}}/wg-server" class="nav-link {{if eq .baseData.Active "wg-server" }}active{{end}}">
<i class="nav-icon fas fa-server"></i> <i class="nav-icon fas fa-server"></i>
@ -117,6 +134,8 @@
</p> </p>
</a> </a>
</li> </li>
<li class="nav-header">SETTINGS</li> <li class="nav-header">SETTINGS</li>
<li class="nav-item"> <li class="nav-item">
<a href="{{.basePath}}/global-settings" class="nav-link {{if eq .baseData.Active "global-settings" }}active{{end}}"> <a href="{{.basePath}}/global-settings" class="nav-link {{if eq .baseData.Active "global-settings" }}active{{end}}">
@ -126,6 +145,16 @@
</p> </p>
</a> </a>
</li> </li>
<li class="nav-item">
<a href="{{.basePath}}/users-settings" class="nav-link {{if eq .baseData.Active "users-settings" }}active{{end}}">
<i class="nav-icon fas fa-cog"></i>
<p>
Users Settings
</p>
</a>
</li>
{{end}}
<li class="nav-header">UTILITIES</li> <li class="nav-header">UTILITIES</li>
<li class="nav-item"> <li class="nav-item">
<a href="{{.basePath}}/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}"> <a href="{{.basePath}}/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}">
@ -331,6 +360,32 @@
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
}) })
$(document).ready(function () {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/test-hash',
dataType: 'json',
contentType: "application/json",
success: function(data) {
if (data.status) {
$("#apply-config-button").show()
}
else
{
$("#apply-config-button").hide()
}
},
error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
// populateClient function for render new client info // populateClient function for render new client info
// on the client page. // on the client page.
function populateClient(client_id) { function populateClient(client_id) {

View file

@ -70,17 +70,8 @@ Wireguard Clients
</div> </div>
<div class="modal-body"> <div class="modal-body">
<input type="hidden" id="qr_client_id" name="qr_client_id"> <input type="hidden" id="qr_client_id" name="qr_client_id">
<a href="" download="" id="qr_code_a"> <img id="qr_code" class="w-100" style="image-rendering: pixelated;" src="" alt="QR code" />
<img id="qr_code" class="w-100" style="image-rendering: pixelated;" src="" alt="QR code" /> <!-- do not include FwMark in any client configs: it is INVALID. -->
</a>
<div class="form-group">
<div class="icheck-primary d-inline">
<input type="checkbox" id="qr_include_fwmark" onchange="regenerateQRCode()">
<label for="qr_include_fwmark">
Include FwMark
</label>
</div>
</div>
</div> </div>
</div> </div>
<!-- /.modal-content --> <!-- /.modal-content -->
@ -263,6 +254,7 @@ Wireguard Clients
// hide all clients and display only the ones that meet the search criteria (name, email, IP) // hide all clients and display only the ones that meet the search criteria (name, email, IP)
$('#search-input').keyup(function () { $('#search-input').keyup(function () {
$("#status-selector").val("All");
var query = $(this).val(); var query = $(this).val();
$('.col-lg-4').hide(); $('.col-lg-4').hide();
$(".info-box-text").each(function() { $(".info-box-text").each(function() {
@ -274,6 +266,70 @@ Wireguard Clients
$(".badge-secondary").filter(':contains("' + query + '")').parent().parent().parent().show(); $(".badge-secondary").filter(':contains("' + query + '")').parent().parent().parent().show();
}) })
$("#status-selector").on('change', function () {
$('#search-input').val("");
switch ($("#status-selector").val()) {
case "All":
$('.col-lg-4').show();
break;
case "Enabled":
$('.col-lg-4').hide();
$('[id^="paused_"]').each(function () {
if ($(this).css("visibility") === "hidden") {
$(this).parent().parent().show();
}
});
break;
case "Disabled":
$('.col-lg-4').hide();
$('[id^="paused_"]').each(function () {
if ($(this).css("visibility") !== "hidden") {
$(this).parent().parent().show();
}
});
break;
case "Connected":
$('.col-lg-4').hide();
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/status',
success: function (data) {
const returnedHTML = $(data).find(".table-success").get();
var returnedString = "";
returnedHTML.forEach(entry => returnedString += entry.outerHTML);
$(".fa-key").each(function () {
if (returnedString.indexOf($(this).parent().text().trim()) != -1) {
$(this).closest('.col-lg-4').show();
}
})
}
});
break;
case "Disconnected":
$('.col-lg-4').show();
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/status',
success: function (data) {
const returnedHTML = $(data).find(".table-success").get();
var returnedString = "";
returnedHTML.forEach(entry => returnedString += entry.outerHTML);
$(".fa-key").each(function () {
if (returnedString.indexOf($(this).parent().text().trim()) != -1) {
$(this).closest('.col-lg-4').hide();
}
})
}
});
break;
default:
$('.col-lg-4').show();
break;
}
});
// modal_pause_client modal event // modal_pause_client modal event
$("#modal_pause_client").on('show.bs.modal', function (event) { $("#modal_pause_client").on('show.bs.modal', function (event) {
const button = $(event.relatedTarget); const button = $(event.relatedTarget);
@ -425,7 +481,7 @@ Wireguard Clients
cache: false, cache: false,
method: 'GET', method: 'GET',
url: '{{.basePath}}/api/client/' + client_id, url: '{{.basePath}}/api/client/' + client_id,
data: { data: {
qrCodeIncludeFwMark: include_fwmark qrCodeIncludeFwMark: include_fwmark
}, },
dataType: 'json', dataType: 'json',

View file

@ -56,10 +56,10 @@ Global Settings
value="{{if .globalSettings.PersistentKeepalive }}{{ .globalSettings.PersistentKeepalive }}{{end}}"> value="{{if .globalSettings.PersistentKeepalive }}{{ .globalSettings.PersistentKeepalive }}{{end}}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="forward_mark">Forward Mark</label> <label for="firewall_mark">Firewall Mark</label>
<input type="text" class="form-control" id="forward_mark" <input type="text" class="form-control" id="firewall_mark"
name="forward_mark" placeholder="Forward Mark" name="firewall_mark" placeholder="Firewall Mark"
value="{{ .globalSettings.ForwardMark }}"> value="{{ .globalSettings.FirewallMark }}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="config_file_path">Wireguard Config File Path</label> <label for="config_file_path">Wireguard Config File Path</label>
@ -100,8 +100,8 @@ Global Settings
until they reach out to other peers themselves. Adding <code>PersistentKeepalive</code> until they reach out to other peers themselves. Adding <code>PersistentKeepalive</code>
can ensure that the connection remains open.</dd> can ensure that the connection remains open.</dd>
<dd>Leave blank to omit this setting in the Client config.</dd> <dd>Leave blank to omit this setting in the Client config.</dd>
<dt>5. Forward Mark</dt> <dt>5. Firewall Mark</dt>
<dd>Set an <code>fwmark</code> on all packets going out of WireGuard's UDP socket. Default value: <code>0xca6c</code></dd> <dd>Add a matching <code>fwmark</code> on all packets going out of a WireGuard non-default-route tunnel. Default value: <code>0xca6c</code></dd>
<dt>6. Wireguard Config File Path</dt> <dt>6. Wireguard Config File Path</dt>
<dd>The path of your Wireguard server config file. Please make sure the parent directory <dd>The path of your Wireguard server config file. Please make sure the parent directory
exists and is writable.</dd> exists and is writable.</dd>
@ -149,9 +149,9 @@ Global Settings
const dns_servers = $("#dns_servers").val().split(","); const dns_servers = $("#dns_servers").val().split(",");
const mtu = $("#mtu").val(); const mtu = $("#mtu").val();
const persistent_keepalive = $("#persistent_keepalive").val(); const persistent_keepalive = $("#persistent_keepalive").val();
const forward_mark = $("#forward_mark").val(); const firewall_mark = $("#firewall_mark").val();
const config_file_path = $("#config_file_path").val(); const config_file_path = $("#config_file_path").val();
const data = {"endpoint_address": endpoint_address, "dns_servers": dns_servers, "mtu": mtu, "persistent_keepalive": persistent_keepalive, "forward_mark": forward_mark, "config_file_path": config_file_path}; const data = {"endpoint_address": endpoint_address, "dns_servers": dns_servers, "mtu": mtu, "persistent_keepalive": persistent_keepalive, "firewall_mark": firewall_mark, "config_file_path": config_file_path};
$.ajax({ $.ajax({
cache: false, cache: false,
@ -222,7 +222,7 @@ Global Settings
config_file_path: { config_file_path: {
required: true required: true
}, },
forward_mark: { firewall_mark: {
required: false required: false
} }
}, },

View file

@ -101,7 +101,11 @@
$("#btn_login").click(function () { $("#btn_login").click(function () {
const username = $("#username").val(); const username = $("#username").val();
const password = $("#password").val(); const password = $("#password").val();
const data = {"username": username, "password": password} let rememberMe = false;
if ($("#remember").is(':checked')){
rememberMe = true;
}
const data = {"username": username, "password": password, "rememberMe": rememberMe}
$.ajax({ $.ajax({
cache: false, cache: false,

View file

@ -31,7 +31,7 @@ Profile
<div class="form-group"> <div class="form-group">
<label for="username" class="control-label">Username</label> <label for="username" class="control-label">Username</label>
<input type="text" class="form-control" name="username" id="username" <input type="text" class="form-control" name="username" id="username"
value="{{ .userInfo.Username }}"> value="">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password" class="control-label">Password</label> <label for="password" class="control-label">Password</label>
@ -55,56 +55,82 @@ Profile
{{ define "bottom_js"}} {{ define "bottom_js"}}
<script> <script>
function updateUserInfo() { {
const username = $("#username").val(); var previous_username;
const password = $("#password").val(); var admin;
const data = {"username": username, "password": password}; }
$.ajax({ $(document).ready(function () {
cache: false, $.ajax({
method: 'POST', cache: false,
url: '{{.basePath}}/profile', method: 'GET',
dataType: 'json', url: '{{.basePath}}/api/user/{{.baseData.CurrentUser}}',
contentType: "application/json", dataType: 'json',
data: JSON.stringify(data), contentType: "application/json",
success: function (data) { success: function (resp) {
toastr.success("Updated admin user information successfully"); const user = resp;
}, $("#username").val(user.username);
error: function (jqXHR, exception) { previous_username = user.username;
const responseJson = jQuery.parseJSON(jqXHR.responseText); admin = user.admin;
toastr.error(responseJson['message']); },
} error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}); });
}
$(document).ready(function () {
$.validator.setDefaults({ function updateUserInfo() {
submitHandler: function () { const username = $("#username").val();
updateUserInfo(); const password = $("#password").val();
} const data = {"username": username, "password": password, "previous_username": previous_username, "admin":admin};
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/update-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
toastr.success("Updated user information successfully");
location.reload();
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
$(document).ready(function () {
$.validator.setDefaults({
submitHandler: function () {
updateUserInfo();
}
});
$("#frm_profile").validate({
rules: {
username: {
required: true
}
},
messages: {
username: {
required: "Please enter a username",
}
},
errorElement: 'span',
errorPlacement: function (error, element) {
error.addClass('invalid-feedback');
element.closest('.form-group').append(error);
},
highlight: function (element, errorClass, validClass) {
$(element).addClass('is-invalid');
},
unhighlight: function (element, errorClass, validClass) {
$(element).removeClass('is-invalid');
}
});
}); });
$("#frm_profile").validate({
rules: {
username: {
required: true
}
},
messages: {
username: {
required: "Please enter a username",
}
},
errorElement: 'span',
errorPlacement: function (error, element) {
error.addClass('invalid-feedback');
element.closest('.form-group').append(error);
},
highlight: function (element, errorClass, validClass) {
$(element).addClass('is-invalid');
},
unhighlight: function (element, errorClass, validClass) {
$(element).removeClass('is-invalid');
}
});
});
</script> </script>
{{ end }} {{ end }}

View file

@ -0,0 +1,294 @@
{{define "title"}}
Users Settings
{{end}}
{{define "top_css"}}
{{end}}
{{define "username"}}
{{ .username }}
{{end}}
{{define "page_title"}}
Users Settings
{{end}}
{{define "page_content"}}
<section class="content">
<div class="container-fluid">
<div class="row" id="users-list">
</div>
</div>
</section>
<div class="modal fade" id="modal_edit_user">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Edit User</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form name="frm_edit_user" id="frm_edit_user">
<div class="modal-body">
<div class="form-group" style="display:none">
<input type="text" style="display:none" class="form-control" id="_previous_user_name"
name="_previous_user_name">
</div>
<div class="form-group">
<label for="_user_name" class="control-label">Name</label>
<input type="text" class="form-control" id="_user_name" name="_user_name">
</div>
<div class="form-group">
<label for="_user_password" class="control-label">Password</label>
<input type="password" class="form-control" id="_user_password" name="_user_password" value=""
placeholder="Leave empty to keep the password unchanged">
</div>
<div class="form-group">
<div class="icheck-primary d-inline">
<input type="checkbox" id="_admin">
<label for="_admin">
Admin
</label>
</div>
</div>
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success">Save</button>
</div>
</form>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
<div class="modal fade" id="modal_remove_user">
<div class="modal-dialog">
<div class="modal-content bg-danger">
<div class="modal-header">
<h4 class="modal-title">Remove</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-outline-dark" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-dark" id="remove_user_confirm">Apply</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
{{end}}
{{define "bottom_js"}}
<script>
function populateUsersList() {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/getusers',
dataType: 'json',
contentType: "application/json",
success: function (data) {
renderUserList(data);
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
</script>
<script>
// load user list
$(document).ready(function () {
populateUsersList();
let newUserHtml = '<div class="col-sm-2 offset-md-4" style=" text-align: right;">' +
'<button style="" id="btn_new_user" type="button" class="btn btn-outline-primary btn-sm" ' +
'data-toggle="modal" data-target="#modal_edit_user" data-username="">' +
'<i class="nav-icon fas fa-plus"></i> New User</button></div>';
$('h1').parents(".row").append(newUserHtml);
})
// modal_remove_user modal event
$("#modal_remove_user").on('show.bs.modal', function (event) {
const button = $(event.relatedTarget);
const user_name = button.data('username');
const modal = $(this);
modal.find('.modal-body').text("You are about to remove user " + user_name);
modal.find('#remove_user_confirm').val(user_name);
})
// remove_user_confirm button event
$(document).ready(function () {
$("#remove_user_confirm").click(function () {
const user_name = $(this).val();
const data = {"username": user_name};
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/remove-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
$("#modal_remove_user").modal('hide');
toastr.success('Removed user successfully');
const divElement = document.getElementById('user_' + user_name);
divElement.style.display = "none";
location.reload()
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
});
// Edit user modal event
$(document).ready(function () {
$("#modal_edit_user").on('show.bs.modal', function (event) {
let modal = $(this);
const button = $(event.relatedTarget);
const user_name = button.data('username');
// update user modal data
if (user_name !== "") {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/api/user/' + user_name,
dataType: 'json',
contentType: "application/json",
success: function (resp) {
const user = resp;
modal.find(".modal-title").text("Edit user " + user.username);
modal.find("#_user_name").val(user.username);
modal.find("#_previous_user_name").val(user.username);
modal.find("#_user_password").val("");
modal.find("#_user_password").prop("placeholder", "Leave empty to keep the password unchanged")
modal.find("#_admin").prop("checked", user.admin);
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
} else {
modal.find(".modal-title").text("Add new user");
modal.find("#_user_name").val("");
modal.find("#_previous_user_name").val("");
modal.find("#_user_password").val("");
modal.find("#_user_password").prop("placeholder", "")
modal.find("#_admin").prop("checked", false);
}
});
});
function updateUserInfo() {
const username = $("#_user_name").val();
const previous_username = $("#_previous_user_name").val();
const password = $("#_user_password").val();
let admin = false;
if ($("#_admin").is(':checked')) {
admin = true;
}
const data = {
"username": username,
"password": password,
"previous_username": previous_username,
"admin": admin
};
if (previous_username !== "") {
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/update-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
toastr.success("Updated user information successfully");
location.reload();
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
} else {
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/create-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
toastr.success("Created user successfully");
location.reload();
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
}
$(document).ready(function () {
$.validator.setDefaults({
submitHandler: function (form) {
updateUserInfo();
}
});
// Edit user form validation
$("#frm_edit_user").validate({
rules: {
_user_name: {
required: true
},
_user_password: {
required: function () {
return $("#_previous_user_name").val() === "";
}
},
},
messages: {
_user_name: {
required: "Please enter a username"
},
_user_password: {
required: "Please input a password"
},
},
errorElement: 'span',
errorPlacement: function (error, element) {
error.addClass('invalid-feedback');
element.closest('.form-group').append(error);
},
highlight: function (element, errorClass, validClass) {
$(element).addClass('is-invalid');
},
unhighlight: function (element, errorClass, validClass) {
$(element).removeClass('is-invalid');
}
});
//
});
</script>
{{end}}

View file

@ -25,12 +25,13 @@ var (
const ( const (
DefaultUsername = "admin" DefaultUsername = "admin"
DefaultPassword = "admin" DefaultPassword = "admin"
DefaultIsAdmin = true
DefaultServerAddress = "10.252.1.0/24" DefaultServerAddress = "10.252.1.0/24"
DefaultServerPort = 51820 DefaultServerPort = 51820
DefaultDNS = "1.1.1.1" DefaultDNS = "1.1.1.1"
DefaultMTU = 1450 DefaultMTU = 1450
DefaultPersistentKeepalive = 15 DefaultPersistentKeepalive = 15
DefaultForwardMark = "0xca6c" DefaultFirewallMark = "0xca6c" // i.e. 51820
DefaultConfigFilePath = "/etc/wireguard/wg0.conf" DefaultConfigFilePath = "/etc/wireguard/wg0.conf"
UsernameEnvVar = "WGUI_USERNAME" UsernameEnvVar = "WGUI_USERNAME"
PasswordEnvVar = "WGUI_PASSWORD" PasswordEnvVar = "WGUI_PASSWORD"
@ -40,8 +41,9 @@ const (
DNSEnvVar = "WGUI_DNS" DNSEnvVar = "WGUI_DNS"
MTUEnvVar = "WGUI_MTU" MTUEnvVar = "WGUI_MTU"
PersistentKeepaliveEnvVar = "WGUI_PERSISTENT_KEEPALIVE" PersistentKeepaliveEnvVar = "WGUI_PERSISTENT_KEEPALIVE"
ForwardMarkEnvVar = "WGUI_FORWARD_MARK" FirewallMarkEnvVar = "WGUI_FIREWALL_MARK"
ConfigFilePathEnvVar = "WGUI_CONFIG_FILE_PATH" ConfigFilePathEnvVar = "WGUI_CONFIG_FILE_PATH"
LogLevel = "WGUI_LOG_LEVEL"
ServerAddressesEnvVar = "WGUI_SERVER_INTERFACE_ADDRESSES" ServerAddressesEnvVar = "WGUI_SERVER_INTERFACE_ADDRESSES"
ServerListenPortEnvVar = "WGUI_SERVER_LISTEN_PORT" ServerListenPortEnvVar = "WGUI_SERVER_LISTEN_PORT"
ServerPostUpScriptEnvVar = "WGUI_SERVER_POST_UP_SCRIPT" ServerPostUpScriptEnvVar = "WGUI_SERVER_POST_UP_SCRIPT"

View file

@ -4,15 +4,20 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/ngoduykhanh/wireguard-ui/store"
"golang.org/x/mod/sumdb/dirhash"
"io"
"io/fs"
"io/ioutil" "io/ioutil"
"net" "net"
"os" "os"
"path"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"text/template" "text/template"
"time" "time"
rice "github.com/GeertJohan/go.rice"
externalip "github.com/glendc/go-external-ip" externalip "github.com/glendc/go-external-ip"
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
"github.com/alikhanich/wireguard-ui/model" "github.com/alikhanich/wireguard-ui/model"
@ -60,18 +65,12 @@ func BuildClientConfig(client model.Client, server model.Server, setting model.G
peerPersistentKeepalive = fmt.Sprintf("PersistentKeepalive = %d\n", setting.PersistentKeepalive) peerPersistentKeepalive = fmt.Sprintf("PersistentKeepalive = %d\n", setting.PersistentKeepalive)
} }
forwardMark := ""
if setting.ForwardMark != "" {
forwardMark = fmt.Sprintf("FwMark = %s\n", setting.ForwardMark)
}
// build the config as string // build the config as string
strConfig := "[Interface]\n" + strConfig := "[Interface]\n" +
clientAddress + clientAddress +
clientPrivateKey + clientPrivateKey +
clientDNS + clientDNS +
clientMTU + clientMTU +
forwardMark +
"\n[Peer]\n" + "\n[Peer]\n" +
peerPublicKey + peerPublicKey +
peerPresharedKey + peerPresharedKey +
@ -221,10 +220,12 @@ func GetPublicIP() (model.Interface, error) {
ip, err := consensus.ExternalIP() ip, err := consensus.ExternalIP()
if err != nil { if err != nil {
publicInterface.IPAddress = "N/A" publicInterface.IPAddress = "N/A"
} else {
publicInterface.IPAddress = ip.String()
} }
publicInterface.IPAddress = ip.String()
return publicInterface, err // error handling happend above, no need to pass it through
return publicInterface, nil
} }
// GetIPFromCIDR get ip from CIDR // GetIPFromCIDR get ip from CIDR
@ -381,7 +382,7 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip
} }
// WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf // WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, clientDataList []model.ClientData, globalSettings model.GlobalSetting) error { func WriteWireGuardServerConfig(tmplDir fs.FS, serverConfig model.Server, clientDataList []model.ClientData, usersList []model.User, globalSettings model.GlobalSetting) error {
var tmplWireguardConf string var tmplWireguardConf string
// if set, read wg.conf template from WgConfTemplate // if set, read wg.conf template from WgConfTemplate
@ -393,7 +394,7 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl
tmplWireguardConf = string(fileContentBytes) tmplWireguardConf = string(fileContentBytes)
} else { } else {
// read default wg.conf template file to string // read default wg.conf template file to string
fileContent, err := tmplBox.String("wg.conf") fileContent, err := StringFromEmbedFile(tmplDir, "wg.conf")
if err != nil { if err != nil {
return err return err
} }
@ -416,6 +417,7 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl
"serverConfig": serverConfig, "serverConfig": serverConfig,
"clientDataList": clientDataList, "clientDataList": clientDataList,
"globalSettings": globalSettings, "globalSettings": globalSettings,
"usersList": usersList,
} }
err = t.Execute(f, config) err = t.Execute(f, config)
@ -462,3 +464,68 @@ func LookupEnvOrStrings(key string, defaultVal []string) []string {
} }
return defaultVal return defaultVal
} }
func StringFromEmbedFile(embed fs.FS, filename string) (string, error) {
file, err := embed.Open(filename)
if err != nil {
return "", err
}
content, err := io.ReadAll(file)
if err != nil {
return "", err
}
return string(content), nil
}
func ParseLogLevel(lvl string) (log.Lvl, error) {
switch strings.ToLower(lvl) {
case "debug":
return log.DEBUG, nil
case "info":
return log.INFO, nil
case "warn":
return log.WARN, nil
case "error":
return log.ERROR, nil
case "off":
return log.OFF, nil
default:
return log.DEBUG, fmt.Errorf("not a valid log level: %s", lvl)
}
}
// GetCurrentHash returns current hashes
func GetCurrentHash(db store.IStore) (string, string) {
hashClients, _ := dirhash.HashDir(path.Join(db.GetPath(), "clients"), "prefix", dirhash.Hash1)
files := append([]string(nil), "prefix/global_settings.json", "prefix/interfaces.json", "prefix/keypair.json")
osOpen := func(name string) (io.ReadCloser, error) {
return os.Open(filepath.Join(path.Join(db.GetPath(), "server"), strings.TrimPrefix(name, "prefix")))
}
hashServer, _ := dirhash.Hash1(files, osOpen)
return hashClients, hashServer
}
func HashesChanged(db store.IStore) bool {
old, _ := db.GetHashes()
oldClient := old.Client
oldServer := old.Server
newClient, newServer := GetCurrentHash(db)
if oldClient != newClient {
//fmt.Println("Hash for client differs")
return true
}
if oldServer != newServer {
//fmt.Println("Hash for server differs")
return true
}
return false
}
func UpdateHashes(db store.IStore) error {
var clientServerHashes model.ClientServerHashes
clientServerHashes.Client, clientServerHashes.Server = GetCurrentHash(db)
return db.SaveHashes(clientServerHashes)
}