diff --git a/.dockerignore b/.dockerignore index b9ff5d8..7624d3e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -25,3 +25,6 @@ docker-compose* db assets wireguard-ui + +# Examples +examples diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e5d93a..feb4c93 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,20 +40,12 @@ jobs: node-version: '14' registry-url: 'https://registry.npmjs.org' - # prepare assets for go rice + # prepare assets - name: Prepare assets run: | chmod +x ./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 - name: Build and make the releases uses: wangyoucao577/go-release-action@master @@ -62,6 +54,7 @@ jobs: goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} goversion: "https://dl.google.com/go/go1.16.1.linux-amd64.tar.gz" + pre_command: export CGO_ENABLED=0 binary_name: "wireguard-ui" 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 }} diff --git a/.gitignore b/.gitignore index d6eba78..f1e3f44 100644 --- a/.gitignore +++ b/.gitignore @@ -16,8 +16,11 @@ wireguard-ui vendor/ assets/ node_modules/ -rice-box.go # IDEs .vscode .idea + +# Examples +examples/docker-compose/config +examples/docker-compose/db diff --git a/Dockerfile b/Dockerfile index a30cefe..e42603b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ LABEL maintainer="Khanh Ngo
${obj.Client.name} + ${obj.Client.email} ${prettyDateTime(obj.Client.created_at)} @@ -78,6 +79,34 @@ function renderClientList(data) { }); } +function renderUserList(data) { + $.each(data, function(index, obj) { + let clientStatusHtml = '>' + + // render user html content + let html = `
+
+
+
+ +
+
+ +
+
+ ${obj.username} + ${obj.admin? 'Administrator':'Manager'} +
+
+
` + + // add the user html elements to the list + $('#users-list').append(html); + }); +} + + function prettyDateTime(timeStr) { const dt = new Date(timeStr); const offsetMs = dt.getTimezoneOffset() * 60 * 1000; diff --git a/examples/docker-compose/README.md b/examples/docker-compose/README.md new file mode 100644 index 0000000..951df08 --- /dev/null +++ b/examples/docker-compose/README.md @@ -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). diff --git a/examples/docker-compose/boringtun.yml b/examples/docker-compose/boringtun.yml new file mode 100644 index 0000000..a1bdd2f --- /dev/null +++ b/examples/docker-compose/boringtun.yml @@ -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" diff --git a/examples/docker-compose/linuxserver.yml b/examples/docker-compose/linuxserver.yml new file mode 100644 index 0000000..1b7a66f --- /dev/null +++ b/examples/docker-compose/linuxserver.yml @@ -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 diff --git a/examples/docker-compose/system.yml b/examples/docker-compose/system.yml new file mode 100644 index 0000000..c27f31e --- /dev/null +++ b/examples/docker-compose/system.yml @@ -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 diff --git a/go.mod b/go.mod index 8e86866..5c3fcb0 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,8 @@ module github.com/alikhanich/wireguard-ui -go 1.14 +go 1.16 require ( - github.com/GeertJohan/go.rice v1.0.2 github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd github.com/go-playground/universal-translator v0.17.0 // indirect github.com/gorilla/sessions v1.2.0 @@ -19,7 +18,8 @@ require ( github.com/sendgrid/sendgrid-go v3.10.0+incompatible github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 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/wgctrl v0.0.0-20210803171230-4253848d036c gopkg.in/go-playground/assert.v1 v1.2.1 // indirect diff --git a/go.sum b/go.sum index beffdb5..ee957cb 100644 --- a/go.sum +++ b/go.sum @@ -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/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/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= @@ -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/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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/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 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/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 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 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/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= 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.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/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/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= 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= @@ -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-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-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-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-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-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-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-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= @@ -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-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-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM= 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-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-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-20181116152217-5ac8a444bdc5/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-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-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-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-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-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.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.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 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-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-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 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/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ= 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/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= diff --git a/handler/routes.go b/handler/routes.go index 597174d..886efce 100644 --- a/handler/routes.go +++ b/handler/routes.go @@ -5,13 +5,13 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io/fs" "net/http" "os" "sort" "strings" "time" - rice "github.com/GeertJohan/go.rice" "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" @@ -52,39 +52,54 @@ func LoginPage() echo.HandlerFunc { // Login for signing in handler func Login(db store.IStore) echo.HandlerFunc { return func(c echo.Context) error { - user := new(model.User) - c.Bind(user) + data := make(map[string]interface{}) + 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 { 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 if dbuser.PasswordHash != "" { - match, err := util.VerifyHash(dbuser.PasswordHash, user.Password) + match, err := util.VerifyHash(dbuser.PasswordHash, password) if err != nil { return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify password"}) } passwordCorrect = match } else { - passwordCorrect = subtle.ConstantTimeCompare([]byte(user.Password), []byte(dbuser.Password)) == 1 + passwordCorrect = subtle.ConstantTimeCompare([]byte(password), []byte(dbuser.Password)) == 1 } if userCorrect && passwordCorrect { // 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.Options = &sessions.Options{ Path: util.BasePath, - MaxAge: 86400, + MaxAge: ageMax, HttpOnly: true, } // set session_token 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.Save(c.Request(), c.Response()) @@ -92,7 +107,7 @@ func Login(db store.IStore) echo.HandlerFunc { cookie := new(http.Cookie) cookie.Name = "session_token" cookie.Value = tokenUID - cookie.Expires = time.Now().Add(24 * time.Hour) + cookie.Expires = expiration c.SetCookie(cookie) 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 func Logout() echo.HandlerFunc { return func(c echo.Context) error { @@ -113,21 +162,23 @@ func Logout() echo.HandlerFunc { // LoadProfile to load user information func LoadProfile(db store.IStore) echo.HandlerFunc { 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{}{ - "baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c)}, - "userInfo": userInfo, + "baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c), Admin: isAdmin(c)}, }) } } -// UpdateProfile to update user information -func UpdateProfile(db store.IStore) echo.HandlerFunc { +// UsersSettings handler +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 { data := make(map[string]interface{}) err := json.NewDecoder(c.Request().Body).Decode(&data) @@ -138,8 +189,18 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc { username := data["username"].(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 { return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) } @@ -150,6 +211,13 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc { 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 != "" { hash, err := util.HashPassword(password) if err != nil { @@ -158,12 +226,96 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc { 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 { 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{}{ - "baseData": model.BaseData{Active: "", CurrentUser: currentUser(c)}, + "baseData": model.BaseData{Active: "", CurrentUser: currentUser(c), Admin: isAdmin(c)}, "clientDataList": clientDataList, }) } @@ -532,7 +684,7 @@ func WireGuardServer(db store.IStore) echo.HandlerFunc { } 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, "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{}{ - "baseData": model.BaseData{Active: "global-settings", CurrentUser: currentUser(c)}, + "baseData": model.BaseData{Active: "global-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, "globalSettings": globalSettings, }) } @@ -630,7 +782,7 @@ func Status(db store.IStore) echo.HandlerFunc { wgClient, err := wgctrl.New() if err != nil { 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(), "devices": nil, }) @@ -639,7 +791,7 @@ func Status(db store.IStore) echo.HandlerFunc { devices, err := wgClient.Devices() if err != nil { 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(), "devices": nil, }) @@ -651,7 +803,7 @@ func Status(db store.IStore) echo.HandlerFunc { clients, err := db.GetClients(false) if err != nil { 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(), "devices": nil, }) @@ -697,7 +849,7 @@ func Status(db store.IStore) echo.HandlerFunc { } 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, "error": "", }) @@ -796,7 +948,7 @@ func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { } // 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 { 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"}) } + 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() if err != nil { log.Error("Cannot get global settings: ", err) @@ -818,7 +976,7 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc { } // Write config file - err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings) + err = util.WriteWireGuardServerConfig(tmplDir, server, clients, users, settings) if err != nil { log.Error("Cannot apply server config: ", err) 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"}) } } + +// 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 func AboutPage() echo.HandlerFunc { return func(c echo.Context) error { 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)}, }) } } - diff --git a/handler/routes_wake_on_lan.go b/handler/routes_wake_on_lan.go index 4428b91..c9b5160 100644 --- a/handler/routes_wake_on_lan.go +++ b/handler/routes_wake_on_lan.go @@ -37,7 +37,7 @@ func GetWakeOnLanHosts(db store.IStore) echo.HandlerFunc { } 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, "error": "", }) diff --git a/handler/session.go b/handler/session.go index 751c01b..0a483ca 100644 --- a/handler/session.go +++ b/handler/session.go @@ -14,15 +14,24 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc { if !isValidSession(c) { nextURL := c.Request().URL 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 { - return c.Redirect(http.StatusTemporaryRedirect, util.BasePath + "/login") + return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login") } } 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 { if util.DisableLogin { return true @@ -50,10 +59,29 @@ func currentUser(c echo.Context) string { 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 func clearSession(c echo.Context) { sess, _ := session.Get("session", c) sess.Values["username"] = "" + sess.Values["admin"] = false sess.Values["session_token"] = "" sess.Save(c.Request(), c.Response()) } diff --git a/main.go b/main.go index e88bcbd..364393a 100644 --- a/main.go +++ b/main.go @@ -1,15 +1,16 @@ package main import ( + "embed" "flag" "fmt" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" - "github.com/alikhanich/wireguard-ui/store" + "github.com/ngoduykhanh/wireguard-ui/store" + "io/fs" "net/http" "os" "time" - rice "github.com/GeertJohan/go.rice" "github.com/alikhanich/wireguard-ui/emailer" "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() { // command-line flags and env variables @@ -119,19 +130,18 @@ func main() { // set app extra data extraData := make(map[string]string) extraData["appVersion"] = appVersion + extraData["gitCommit"] = gitCommit extraData["basePath"] = util.BasePath - // create rice box for embedded template - tmplBox := rice.MustFindBox("templates") - - // rice file server for assets. "assets" is the folder where the files come from. - assetHandler := http.FileServer(rice.MustFindBox("assets").HTTPBox()) + // strip the "templates/" prefix from the embedded directory so files can be read by their direct name (e.g. + // "base.html" instead of "templates/base.html") + tmplDir, _ := fs.Sub(fs.FS(embeddedTemplates), "templates") // create the wireguard config on start, if it doesn't exist - initServerConfig(db, tmplBox) + initServerConfig(db, tmplDir) // 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) @@ -140,7 +150,12 @@ func main() { app.POST(util.BasePath+"/login", handler.Login(db)) app.GET(util.BasePath+"/logout", handler.Logout(), 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 @@ -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) } + app.GET(util.BasePath+"/test-hash", handler.GetHashesChanges(db), handler.ValidSession) app.GET(util.BasePath+"/about", handler.AboutPage()) app.GET(util.BasePath+"/_health", handler.Health()) 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+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson) app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession) - app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession) - app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson) - app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson) - app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession) - app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson) + 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, handler.NeedsAdmin) + 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, handler.NeedsAdmin) + 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+"/api/clients", handler.GetClients(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/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.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.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.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() if err != nil { 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) } + users, err := db.GetUsers() + if err != nil { + log.Fatalf("Cannot get user config: ", err) + } + // write config file - err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings) + err = util.WriteWireGuardServerConfig(tmplDir, server, clients, users, settings) if err != nil { log.Fatalf("Cannot create server config: ", err) } diff --git a/model/misc.go b/model/misc.go index 12d6906..dc95f15 100644 --- a/model/misc.go +++ b/model/misc.go @@ -10,4 +10,11 @@ type Interface struct { type BaseData struct { Active string CurrentUser string + Admin bool +} + +// ClientServerHashes struct, to save hashes to detect changes +type ClientServerHashes struct { + Client string `json:"client"` + Server string `json:"server"` } diff --git a/model/setting.go b/model/setting.go index e871591..a702293 100644 --- a/model/setting.go +++ b/model/setting.go @@ -10,7 +10,7 @@ type GlobalSetting struct { DNSServers []string `json:"dns_servers"` MTU int `json:"mtu,string"` PersistentKeepalive int `json:"persistent_keepalive,string"` - ForwardMark string `json:"forward_mark"` + FirewallMark string `json:"firewall_mark"` ConfigFilePath string `json:"config_file_path"` UpdatedAt time.Time `json:"updated_at"` } diff --git a/model/user.go b/model/user.go index 711ebd1..71f4d13 100644 --- a/model/user.go +++ b/model/user.go @@ -6,4 +6,5 @@ type User struct { Password string `json:"password"` // PasswordHash takes precedence over Password. PasswordHash string `json:"password_hash"` + Admin bool `json:"admin"` } diff --git a/router/router.go b/router/router.go index 953894f..281e00a 100644 --- a/router/router.go +++ b/router/router.go @@ -3,11 +3,11 @@ package router import ( "errors" "io" + "io/fs" "reflect" "strings" "text/template" - rice "github.com/GeertJohan/go.rice" "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" @@ -66,54 +66,59 @@ func apiKeyMiddleware(next echo.HandlerFunc) echo.HandlerFunc { } } // 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() store := sessions.NewCookieStore(secret) e.Use(session.Middleware(store)) e.Use(apiKeyMiddleware) // read html template file to string - tmplBaseString, err := tmplBox.String("base.html") + tmplBaseString, err := util.StringFromEmbedFile(tmplDir, "base.html") if err != nil { log.Fatal(err) } - tmplLoginString, err := tmplBox.String("login.html") + tmplLoginString, err := util.StringFromEmbedFile(tmplDir, "login.html") if err != nil { log.Fatal(err) } - tmplProfileString, err := tmplBox.String("profile.html") + tmplProfileString, err := util.StringFromEmbedFile(tmplDir, "profile.html") if err != nil { log.Fatal(err) } - tmplClientsString, err := tmplBox.String("clients.html") + tmplClientsString, err := util.StringFromEmbedFile(tmplDir, "clients.html") if err != nil { log.Fatal(err) } - tmplServerString, err := tmplBox.String("server.html") + tmplServerString, err := util.StringFromEmbedFile(tmplDir, "server.html") if err != nil { log.Fatal(err) } - tmplGlobalSettingsString, err := tmplBox.String("global_settings.html") + tmplGlobalSettingsString, err := util.StringFromEmbedFile(tmplDir, "global_settings.html") if err != nil { log.Fatal(err) } - tmplStatusString, err := tmplBox.String("status.html") + tmplUsersSettingsString, err := util.StringFromEmbedFile(tmplDir, "users_settings.html") if err != nil { log.Fatal(err) } - tmplWakeOnLanHostsString, err := tmplBox.String("wake_on_lan_hosts.html") + tmplStatusString, err := util.StringFromEmbedFile(tmplDir, "status.html") if err != nil { 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 { 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["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["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["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)) - 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.Use(middleware.Logger()) + e.Use(middleware.LoggerWithConfig(logConfig)) e.HideBanner = true + e.HidePort = lvl > log.INFO // hide the port output if the log level is higher than INFO e.Validator = NewValidator() e.Renderer = &TemplateRegistry{ templates: templates, diff --git a/store/jsondb/jsondb.go b/store/jsondb/jsondb.go index 3ee16d5..cc9e1ec 100644 --- a/store/jsondb/jsondb.go +++ b/store/jsondb/jsondb.go @@ -42,7 +42,9 @@ func (o *JsonDB) Init() error { var serverInterfacePath string = path.Join(serverPath, "interfaces.json") var serverKeyPairPath string = path.Join(serverPath, "keypair.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") + // create directories if they do not exist if _, err := os.Stat(clientPath); os.IsNotExist(err) { os.MkdirAll(clientPath, os.ModePerm) @@ -53,6 +55,9 @@ func (o *JsonDB) Init() error { if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) { os.MkdirAll(wakeOnLanHostsPath, os.ModePerm) } + if _, err := os.Stat(userPath); os.IsNotExist(err) { + os.MkdirAll(userPath, os.ModePerm) + } // server's interface 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.MTU = util.LookupEnvOrInt(util.MTUEnvVar, util.DefaultMTU) 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.UpdatedAt = time.Now().UTC() 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 - 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.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername) + user.Admin = util.DefaultIsAdmin user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "") if user.PasswordHash == "" { plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword) @@ -115,7 +130,7 @@ func (o *JsonDB) Init() error { } user.PasswordHash = hash } - o.conn.Write("server", "users", user) + o.conn.Write("users", user.Username, user) } return nil @@ -127,9 +142,44 @@ func (o *JsonDB) GetUser() (model.User, error) { 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 { - 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 @@ -213,15 +263,12 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti server, _ := o.GetServer() globalSettings, _ := o.GetGlobalSettings() client := client - if !qrCodeSettings.IncludeDNS{ + if !qrCodeSettings.IncludeDNS { globalSettings.DNSServers = []string{} } if !qrCodeSettings.IncludeMTU { globalSettings.MTU = 0 } - if !qrCodeSettings.IncludeFwMark { - globalSettings.ForwardMark = "" - } png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256) if err == nil { @@ -255,3 +302,16 @@ func (o *JsonDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error { func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error { 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) +} diff --git a/store/store.go b/store/store.go index ec494cc..dec257b 100644 --- a/store/store.go +++ b/store/store.go @@ -6,8 +6,10 @@ import ( type IStore interface { Init() error - GetUser() (model.User, error) + GetUsers() ([]model.User, error) + GetUserByName(username string) (model.User, error) SaveUser(user model.User) error + DeleteUser(username string) error GetGlobalSettings() (model.GlobalSetting, error) GetServer() (model.Server, error) GetClients(hasQRCode bool) ([]model.ClientData, error) @@ -22,4 +24,7 @@ type IStore interface { DeleteWakeOnHostLanHost(macAddress string) error SaveWakeOnLanHost(host model.WakeOnLanHost) error DeleteWakeOnHost(host model.WakeOnLanHost) error + GetPath() string + SaveHashes(hashes model.ClientServerHashes) error + GetHashes() (model.ClientServerHashes, error) } diff --git a/templates/about.html b/templates/about.html index f513607..b57b270 100644 --- a/templates/about.html +++ b/templates/about.html @@ -30,6 +30,12 @@ About +{{ if .gitCommit }} +
+ + +
+{{ end }}
diff --git a/templates/base.html b/templates/base.html index 2cec330..5fd2ce3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -56,6 +56,15 @@
+
+ +
@@ -63,7 +72,7 @@ - {{if .baseData.CurrentUser}} @@ -90,7 +99,13 @@
{{if .baseData.CurrentUser}} - {{.baseData.CurrentUser}} + + {{if .baseData.Admin}} + Administrator: {{.baseData.CurrentUser}} + {{else}} + Manager: {{.baseData.CurrentUser}} + {{end}} + {{else}} Administrator {{end}} @@ -109,6 +124,8 @@

+ + {{if .baseData.Admin}} + + + + {{end}} +
@@ -263,6 +254,7 @@ Wireguard Clients // hide all clients and display only the ones that meet the search criteria (name, email, IP) $('#search-input').keyup(function () { + $("#status-selector").val("All"); var query = $(this).val(); $('.col-lg-4').hide(); $(".info-box-text").each(function() { @@ -274,6 +266,70 @@ Wireguard Clients $(".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").on('show.bs.modal', function (event) { const button = $(event.relatedTarget); @@ -425,7 +481,7 @@ Wireguard Clients cache: false, method: 'GET', url: '{{.basePath}}/api/client/' + client_id, - data: { + data: { qrCodeIncludeFwMark: include_fwmark }, dataType: 'json', diff --git a/templates/global_settings.html b/templates/global_settings.html index 8a41d1f..15d7b4b 100644 --- a/templates/global_settings.html +++ b/templates/global_settings.html @@ -56,10 +56,10 @@ Global Settings value="{{if .globalSettings.PersistentKeepalive }}{{ .globalSettings.PersistentKeepalive }}{{end}}">
- - + +
@@ -100,8 +100,8 @@ Global Settings until they reach out to other peers themselves. Adding PersistentKeepalive can ensure that the connection remains open.
Leave blank to omit this setting in the Client config.
-
5. Forward Mark
-
Set an fwmark on all packets going out of WireGuard's UDP socket. Default value: 0xca6c
+
5. Firewall Mark
+
Add a matching fwmark on all packets going out of a WireGuard non-default-route tunnel. Default value: 0xca6c
6. Wireguard Config File Path
The path of your Wireguard server config file. Please make sure the parent directory exists and is writable.
@@ -149,9 +149,9 @@ Global Settings const dns_servers = $("#dns_servers").val().split(","); const mtu = $("#mtu").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 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({ cache: false, @@ -222,7 +222,7 @@ Global Settings config_file_path: { required: true }, - forward_mark: { + firewall_mark: { required: false } }, diff --git a/templates/login.html b/templates/login.html index 8fcc225..6d74a30 100644 --- a/templates/login.html +++ b/templates/login.html @@ -101,7 +101,11 @@ $("#btn_login").click(function () { const username = $("#username").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({ cache: false, diff --git a/templates/profile.html b/templates/profile.html index c2d3b95..fa80157 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -31,7 +31,7 @@ Profile
+ value="">
@@ -55,56 +55,82 @@ Profile {{ define "bottom_js"}} {{ end }} diff --git a/templates/users_settings.html b/templates/users_settings.html new file mode 100644 index 0000000..6fb8d69 --- /dev/null +++ b/templates/users_settings.html @@ -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"}} +
+
+
+
+
+
+ + + + + + +{{end}} + +{{define "bottom_js"}} + + +{{end}} diff --git a/util/config.go b/util/config.go index 447d913..1000055 100644 --- a/util/config.go +++ b/util/config.go @@ -25,12 +25,13 @@ var ( const ( DefaultUsername = "admin" DefaultPassword = "admin" + DefaultIsAdmin = true DefaultServerAddress = "10.252.1.0/24" DefaultServerPort = 51820 DefaultDNS = "1.1.1.1" DefaultMTU = 1450 DefaultPersistentKeepalive = 15 - DefaultForwardMark = "0xca6c" + DefaultFirewallMark = "0xca6c" // i.e. 51820 DefaultConfigFilePath = "/etc/wireguard/wg0.conf" UsernameEnvVar = "WGUI_USERNAME" PasswordEnvVar = "WGUI_PASSWORD" @@ -40,8 +41,9 @@ const ( DNSEnvVar = "WGUI_DNS" MTUEnvVar = "WGUI_MTU" PersistentKeepaliveEnvVar = "WGUI_PERSISTENT_KEEPALIVE" - ForwardMarkEnvVar = "WGUI_FORWARD_MARK" + FirewallMarkEnvVar = "WGUI_FIREWALL_MARK" ConfigFilePathEnvVar = "WGUI_CONFIG_FILE_PATH" + LogLevel = "WGUI_LOG_LEVEL" ServerAddressesEnvVar = "WGUI_SERVER_INTERFACE_ADDRESSES" ServerListenPortEnvVar = "WGUI_SERVER_LISTEN_PORT" ServerPostUpScriptEnvVar = "WGUI_SERVER_POST_UP_SCRIPT" diff --git a/util/util.go b/util/util.go index 7d61f60..81ba6ff 100644 --- a/util/util.go +++ b/util/util.go @@ -4,15 +4,20 @@ import ( "encoding/json" "errors" "fmt" + "github.com/ngoduykhanh/wireguard-ui/store" + "golang.org/x/mod/sumdb/dirhash" + "io" + "io/fs" "io/ioutil" "net" "os" + "path" + "path/filepath" "strconv" "strings" "text/template" "time" - rice "github.com/GeertJohan/go.rice" externalip "github.com/glendc/go-external-ip" "github.com/labstack/gommon/log" "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) } - forwardMark := "" - if setting.ForwardMark != "" { - forwardMark = fmt.Sprintf("FwMark = %s\n", setting.ForwardMark) - } - // build the config as string strConfig := "[Interface]\n" + clientAddress + clientPrivateKey + clientDNS + clientMTU + - forwardMark + "\n[Peer]\n" + peerPublicKey + peerPresharedKey + @@ -221,10 +220,12 @@ func GetPublicIP() (model.Interface, error) { ip, err := consensus.ExternalIP() if err != nil { 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 @@ -381,7 +382,7 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip } // 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 // if set, read wg.conf template from WgConfTemplate @@ -393,7 +394,7 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl tmplWireguardConf = string(fileContentBytes) } else { // read default wg.conf template file to string - fileContent, err := tmplBox.String("wg.conf") + fileContent, err := StringFromEmbedFile(tmplDir, "wg.conf") if err != nil { return err } @@ -416,6 +417,7 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl "serverConfig": serverConfig, "clientDataList": clientDataList, "globalSettings": globalSettings, + "usersList": usersList, } err = t.Execute(f, config) @@ -462,3 +464,68 @@ func LookupEnvOrStrings(key string, defaultVal []string) []string { } 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) +}