Compare commits

...

35 Commits

Author SHA1 Message Date
Christoph Haas
83271b5d34 fix user edit bug, allow to delete users from the database (#40) 2022-03-15 23:34:55 +01:00
Alexis
cc50fcf8e6 Feat/ldap certificate connexion (#92)
* Give the way to connect against LDAP server with certificate and key

* fix(ldap) Update cert variable name

In order to be more explicit

Co-authored-by: Alexis Aurin <alexis@so6.pw>
2022-03-15 22:46:00 +01:00
Christoph Haas
5d4d06db81 fix invalid interface public key (#74) 2021-12-16 19:51:45 +01:00
ultram4rine
e581b3a69f Wireguard exporter friendly tags (#81)
* add friendly name

* add friendly name as option to configuration

* add friendly name configuration to readme
2021-12-16 19:35:15 +01:00
Alexander Beck
acb629f672 do not overwrite preshared key in CreatePeer (#77) 2021-12-10 16:52:44 +01:00
Christoph Haas
b5cb967e09 improve ldap logging (#67) 2021-11-07 13:20:16 +01:00
commonism
5a9918e00d docker-compose - use logging limits (#66)
- ldap sync is very noisy, limits/rotation required
 - can be verified with
   docker inspect -f '{{.HostConfig.LogConfig}}' 88…de
   {json-file map[max-file:3 max-size:10m]}

Co-authored-by: Markus Koetter <koetter@cispa.de>
2021-11-04 22:52:14 +01:00
Christoph Haas
897a2bacf0 circle-ci fix 2021-10-14 21:37:10 +02:00
Christoph Haas
759cf3a0bc build for debian stretch (legacy) and with latest golang version (#61) 2021-10-14 21:25:19 +02:00
Christoph Haas
a07457b41f build for debian stretch (legacy) and with latest golang version (#61) 2021-10-14 21:21:06 +02:00
commonism
d7b52eba1c ldap - compare DNs using DN.Equal (#60)
* ldap - compare DNs using DN.Equal

* ldap/isAdmin- restructure & remove code duplication

Co-authored-by: Markus Koetter <koetter@cispa.de>
2021-10-14 08:57:03 +02:00
commonism
04bc0b7a81 UI unit tests (#59)
* tests - add pytests for the UI

* tests/api - fix NotImplemented

* tests - add README

Co-authored-by: Markus Koetter <koetter@cispa.de>
2021-09-30 22:58:24 +02:00
commonism
19c58fb5af Fixes & API unit testing (#58)
* api - add OperationID

  helps when using pyswagger and is visible via
  http://localhost:8123/swagger/index.html?displayOperationId=true
  gin-swagger can not set displayOperationId yet

* api - match paramters to their property equivalents

  pascalcase & sometimes replacing the name (e.g. device -> DeviceName)

* api - use ShouldBindJSON instead of BindJSON

 BindJSON sets the content-type text/plain

* api - we renamed, we regenerated

* device - allow - in DeviceName wg-example0.conf etc

* api - more pascalcase & argument renames

* api - marshal DeletedAt as string

  gorm.DeletedAt is of type sql.NullTime
  NullTime declares Time & Valid as properties
  DeletedAt marshals as time.Time
  swaggertype allows only basic types
  -> string

* Peer - export UID/DeviceType in json
 UID/DeviceType is required, skipping in json, skips it in marshalling,
 next unmarshalling fails

* assets - name forms for use with mechanize

* api - match error message

* add python3/pyswagger based unittesting
 - initializes a clean install by configuration via web service
 - tests the rest api

* tests - test address exhaustion

* tests - test network expansion

Co-authored-by: Markus Koetter <koetter@cispa.de>
2021-09-29 18:41:13 +02:00
commonism
93db475eee swag - use pascalcase for properties (#54)
Co-authored-by: Markus Koetter <koetter@cispa.de>
2021-09-27 20:28:03 +02:00
The one with the braid (she/her) | Dфҿ mit dem Zopf (sie/ihr)
9147fe33cb Added some more customization options (#43)
* Added some more customization options

* Fixed inconsistent height of custom logos

* Extended navbar style to login page
2021-09-12 10:17:13 +02:00
Christoph Haas
f27909a6ce update dependencies 2021-08-24 21:31:31 +02:00
Christoph Haas
b4bd2b35e2 add HttpOnly and Secure flag to cookie store (#39) 2021-08-24 21:26:16 +02:00
Christoph Haas
929c95f9ae fix version in docker builds 2021-08-24 21:00:13 +02:00
Christoph Haas
7b348888d7 fix version in docker builds 2021-08-24 20:18:13 +02:00
Christoph Haas
5aa777f08d update docker tag names 2021-08-23 23:27:42 +02:00
Christoph Haas
c0abce15d6 also use circleci for tags 2021-08-23 23:03:55 +02:00
h44z
e9369b0afd Circleci project setup (#37) (#38)
* Add .circleci/config.yml

* add go sum, remove travis file

* store artifacts

* github release upload

* github release upload
2021-08-23 22:48:05 +02:00
Christoph Haas
becb35d65e Use Github Actions to build Docker image for hub.docker.com (#26) 2021-08-23 21:47:05 +02:00
Christoph Haas
c0c41bdf2a Use Github Actions to build Docker image for hub.docker.com (#26) 2021-08-23 21:21:05 +02:00
Christoph Haas
57b57931b2 validate user in session (#32) 2021-07-30 13:56:21 +02:00
Christoph Haas
fbc0b26631 sendall button for mails, update icons for peer creation buttons (#35) 2021-07-30 13:43:39 +02:00
Christoph Haas
e6ad82ec6e changed headline to avoid confusion (#33) 2021-07-30 12:32:10 +02:00
Christoph Haas
c3c0971aa0 update dependencies 2021-07-30 12:27:21 +02:00
h44z
16a373f1eb Fix typo 2021-07-20 11:02:17 +02:00
h44z
91b83d7882 Log number of ldap users (#36) 2021-07-20 11:01:03 +02:00
h44z
1e35fb2538 Use Github Container Registry
Also publish docker images on ghcr.io
2021-06-30 17:57:41 +02:00
Christoph Haas
400259a0be convert input to email-token on focus loss (#28) 2021-06-30 17:36:39 +02:00
Christoph Haas
96c713a513 update bootstrap-tokenfield lib, fix enter bug (#27)
related: https://github.com/sliptree/bootstrap-tokenfield/issues/308
2021-06-30 17:28:25 +02:00
Christoph Haas
3645d75d8d fix auto-creation of peers on login (#30) 2021-06-30 17:03:16 +02:00
h44z
a017775f8a Add minimum Go version to Readme (#29) 2021-06-25 17:01:16 +02:00
49 changed files with 2223 additions and 552 deletions

139
.circleci/config.yml Normal file
View File

@@ -0,0 +1,139 @@
version: 2.1
jobs:
build-latest:
steps:
- checkout
- restore_cache:
keys:
- go-mod-latest-v4-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: |
make dep
- save_cache:
key: go-mod-latest-v4-{{ checksum "go.sum" }}
paths:
- "~/go/pkg/mod"
- run:
name: Build AMD64
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build
- run:
name: Install Cross-Platform Dependencies
command: |
sudo apt-get update
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-arm-linux-gnueabihf libc6-dev-armhf-cross gcc-aarch64-linux-gnu libc6-dev-arm64-cross
sudo ln -s /usr/include/asm-generic /usr/include/asm
- run:
name: Build ARM
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-cross-plat
- store_artifacts:
path: ~/repo/dist
- run:
name: "Publish Release on GitHub"
command: |
if [ ! -z "${CIRCLE_TAG}" ]; then
go get github.com/tcnksm/ghr
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace $CIRCLE_TAG ~/repo/dist
fi
working_directory: ~/repo
docker:
- image: cimg/go:1.17
build-116: # just to validate compatibility with minimum go version
steps:
- checkout
- restore_cache:
keys:
- go-mod-116-v4-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: |
make dep
- save_cache:
key: go-mod-116-v4-{{ checksum "go.sum" }}
paths:
- "~/go/pkg/mod"
- run:
name: Build AMD64
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build
working_directory: ~/repo116
docker:
- image: cimg/go:1.16
build-legacy:
steps:
- checkout
- restore_cache:
keys:
- go-mod-legacy-v4-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: |
make dep
- save_cache:
key: go-mod-legacy-v4-{{ checksum "go.sum" }}
paths:
- "/go/pkg/mod"
- run:
name: Build AMD64
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build
- run:
name: Install Cross-Platform Dependencies
command: |
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-arm-linux-gnueabihf libc6-dev-armhf-cross gcc-aarch64-linux-gnu libc6-dev-arm64-cross
sudo ln -s /usr/include/asm-generic /usr/include/asm
- run:
name: Build ARM
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-cross-plat
- store_artifacts:
path: ~/repolegacy/dist
- run:
name: "Publish Legacy Release on GitHub"
command: |
rm ~/repolegacy/dist/wg-portal.service ~/repolegacy/dist/wg-portal.env
mv ~/repolegacy/dist/wg-portal-amd64 ~/repolegacy/dist/wg-portal-amd64-legacy
mv ~/repolegacy/dist/wg-portal-arm ~/repolegacy/dist/wg-portal-arm-legacy
mv ~/repolegacy/dist/wg-portal-arm64 ~/repolegacy/dist/wg-portal-arm64-legacy
if [ ! -z "${CIRCLE_TAG}" ]; then
go get github.com/tcnksm/ghr
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} $CIRCLE_TAG ~/repolegacy/dist
fi
working_directory: ~/repolegacy
docker:
- image: circleci/golang:1.16-stretch
workflows:
build-and-release:
jobs:
#--------------- BUILD ---------------#
- build-latest:
filters:
tags:
only: /^v.*/
- build-116:
requires:
- build-latest
filters:
tags:
only: /^v.*/
- build-legacy:
requires:
- build-latest
filters:
tags:
only: /^v.*/

123
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,123 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
push:
branches: [ master ]
# Publish vX.X.X tags as releases.
tags: [ 'v*.*.*' ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build-dockerhub:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Get Version
shell: bash
run: |
echo "::set-output name=identifier::$(echo ${GITHUB_REF##*/})"
echo "::set-output name=hash::$(echo ${GITHUB_SHA} | cut -c1-7)"
id: get_version
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3
with:
images: h44z/wg-portal
flavor: |
latest=true
prefix=
suffix=
tags: |
type=ref,event=branch
type=ref,event=tag
type=semver,pattern={{version}}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }}
BUILD_VERSION=${{ steps.get_version.outputs.hash }}
build-github:
name: Push Docker image to Github Container Registry
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Get Version
shell: bash
run: |
echo "::set-output name=identifier::$(echo ${GITHUB_REF##*/})"
echo "::set-output name=hash::$(echo ${GITHUB_SHA} | cut -c1-7)"
id: get_version
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=true
prefix=
suffix=
tags: |
type=ref,event=branch
type=ref,event=tag
type=semver,pattern={{version}}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }}
BUILD_VERSION=${{ steps.get_version.outputs.hash }}

4
.gitignore vendored
View File

@@ -31,6 +31,6 @@ data/
ssh.key ssh.key
.testCoverage.txt .testCoverage.txt
wg_portal.db wg_portal.db
go.sum
swagger.json swagger.json
swagger.yaml swagger.yaml
/config.yml

View File

@@ -1,43 +0,0 @@
language: go
dist: bionic
sudo: required
go:
- 1.16.x # Latest go version
env:
- GO111MODULE=on
addons:
apt:
packages:
- gcc-multilib
before_install:
- # skip
install:
- # skip
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .)
- go vet $(go list ./... | grep -v /vendor/)
- make ENV_BUILD_IDENTIFIER=$TRAVIS_TAG ENV_BUILD_VERSION=$(echo $TRAVIS_COMMIT | cut -c1-7) build
# Switch over GCC to cross compilation (breaks 386, hence why do it here only)
- sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-arm-linux-gnueabihf libc6-dev-armhf-cross gcc-aarch64-linux-gnu libc6-dev-arm64-cross
- sudo ln -s /usr/include/asm-generic /usr/include/asm
- make ENV_BUILD_IDENTIFIER=$TRAVIS_TAG ENV_BUILD_VERSION=$(echo $TRAVIS_COMMIT | cut -c1-7) build-cross-plat
deploy:
provider: releases
skip_cleanup: true # Important, otherwise the build output would be purged.
api_key:
# *encrypted* GitHub key, as the output of the Travis CI CLI tool
secure: "uZ7Vg7mEP7aUyaf/Uq5UZt6r3Ig/iOWcf7DZMhAWilOayeqdfW8kp2VzKFgTM6PJJm5Zv+OYV4dniO11QDjIX5sJfS10ApaxvPjw/a4NkqKykfrKZABWRmuvSv/PSjzl7jnWgnPqydBmHCfowsxI6X9j1uivgXZDMYg9BKOnDJtVoakUWJ47GKWr7ZegvF5DwB3EaPwDUmJIAJRiMqO+I2QmuVmLvvzkuhSQ/yuCjel/O7kudJuioJOvsxSHH5Mjh7HZoYayAFikVIGCXJStzMCeLwa+lUHUXoofoDT8SHMmcw2Oil1OpeC1PhvtT6VFLzYl9aphl472F9zP0TlBzR5VJ3+r5dwFVhf0MHp0LflIIg8RGjZg/H60yUUPbGYW7gN3wjdH1l7i66HcqFVs39GgzPCpxNuz8bhhUJOtR6K9FujYpp8AkFCwB327LwGzBLWP3wLGkmhj3ca3FBGJLZhzRdK6gpdp9KgY+33wJ/5R7zsUGtEGTjzsGB1GmBBb887qt0mh/cfm/mdh5HPWvZCif2WTyWd2W8gUiN4oTPhRdE/FRFUqoR1WEZeQrjgj3tThywrXIpRVdigN74UMsnlThSHxPZdJHPLftei2A3b+yfYgxt43sp22MqyuB6K7mT5ximQLWldN2Ibf7kKb5RO9/WX5P8LUj1KXtY3dh2o="
file:
- dist/wg-portal-amd64
- dist/wg-portal-arm64
- dist/wg-portal-arm
- dist/wg-portal.env
- dist/wg-portal.service
on:
repo: h44z/wg-portal
tags: true # The deployment happens only if the commit has a tag.

View File

@@ -52,7 +52,7 @@ docker-push:
docker push $(IMAGE) docker push $(IMAGE)
api-docs: api-docs:
cd internal/server; swag init --parseDependency --parseInternal --generalInfo api.go cd internal/server; swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo api.go
$(GOCMD) fmt internal/server/docs/docs.go $(GOCMD) fmt internal/server/docs/docs.go
$(BUILDDIR)/%-amd64: cmd/%/main.go dep phony $(BUILDDIR)/%-amd64: cmd/%/main.go dep phony

133
README.md
View File

@@ -8,9 +8,9 @@
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/h44z/wg-portal) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/h44z/wg-portal)
[![Docker Pulls](https://img.shields.io/docker/pulls/h44z/wg-portal.svg)](https://hub.docker.com/r/h44z/wg-portal/) [![Docker Pulls](https://img.shields.io/docker/pulls/h44z/wg-portal.svg)](https://hub.docker.com/r/h44z/wg-portal/)
A simple, web based configuration portal for [WireGuard](https://wireguard.com). A simple, web based configuration portal for [WireGuard](https://wireguard.com).
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
connections. connections.
The configuration portal currently supports using SQLite and MySQL as a user source for authentication and profile data. The configuration portal currently supports using SQLite and MySQL as a user source for authentication and profile data.
@@ -31,11 +31,11 @@ It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
* Can be used with existing WireGuard setups * Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces * Support for multiple WireGuard interfaces
* REST API for management and client deployment * REST API for management and client deployment
![Screenshot](screenshot.png) ![Screenshot](screenshot.png)
## Setup ## Setup
Make sure that your host system has at least one WireGuard interface (for example wg0) available. Make sure that your host system has at least one WireGuard interface (for example wg0) available.
If you did not start up a WireGuard interface yet, take a look at [wg-quick](https://manpages.debian.org/unstable/wireguard-tools/wg-quick.8.en.html) in order to get started. If you did not start up a WireGuard interface yet, take a look at [wg-quick](https://manpages.debian.org/unstable/wireguard-tools/wg-quick.8.en.html) in order to get started.
### Docker ### Docker
@@ -87,7 +87,7 @@ If needed, please make sure to back up your files from ```/etc/wireguard```.
For a full list of configuration options take a look at the source file [internal/server/configuration.go](internal/server/configuration.go#L56). For a full list of configuration options take a look at the source file [internal/server/configuration.go](internal/server/configuration.go#L56).
### Standalone ### Standalone
For a standalone application, use the Makefile provided in the repository to build the application. For a standalone application, use the Makefile provided in the repository to build the application. Go version 1.16 or higher has to be installed to build WireGuard Portal.
``` ```
make make
@@ -108,56 +108,61 @@ For example: `CONFIG_FILE=/home/test/config.yml ./wg-portal-amd64`.
### Configuration Options ### Configuration Options
The following configuration options are available: The following configuration options are available:
| environment | yaml | yaml_parent | default_value | description | | environment | yaml | yaml_parent | default_value | description |
|-----------------------|-------------------|-------------|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| |----------------------------|-------------------------|-------------|-------------------------------------------------|-------------------------------------------------------------------------------------------|
| LISTENING_ADDRESS | listeningAddress | core | :8123 | The address on which the web server is listening. Optional IP address and port, e.g.: 127.0.0.1:8080. | | LISTENING_ADDRESS | listeningAddress | core | :8123 | The address on which the web server is listening. Optional IP address and port, e.g.: 127.0.0.1:8080. |
| EXTERNAL_URL | externalUrl | core | http://localhost:8123 | The external URL where the web server is reachable. This link is used in emails that are created by the WireGuard Portal. | | EXTERNAL_URL | externalUrl | core | http://localhost:8123 | The external URL where the web server is reachable. This link is used in emails that are created by the WireGuard Portal. |
| WEBSITE_TITLE | title | core | WireGuard VPN | The website title. | | WEBSITE_TITLE | title | core | WireGuard VPN | The website title. |
| COMPANY_NAME | company | core | WireGuard Portal | The company name (for branding). | | COMPANY_NAME | company | core | WireGuard Portal | The company name (for branding). |
| MAIL_FROM | mailFrom | core | WireGuard VPN <noreply@company.com> | The email address from which emails are sent. | | MAIL_FROM | mailFrom | core | WireGuard VPN <noreply@company.com> | The email address from which emails are sent. |
| ADMIN_USER | adminUser | core | admin@wgportal.local | The administrator user. Must be a valid email address. | | LOGO_URL | logoUrl | core | /img/header-logo.png | The logo displayed in the page's header. |
| ADMIN_PASS | adminPass | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. | | ADMIN_USER | adminUser | core | admin@wgportal.local | The administrator user. Must be a valid email address. |
| EDITABLE_KEYS | editableKeys | core | true | Allow to edit key-pairs in the UI. | | ADMIN_PASS | adminPass | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
| CREATE_DEFAULT_PEER | createDefaultPeer | core | false | If an LDAP user logs in for the first time, a new WireGuard peer will be created on the WG_DEFAULT_DEVICE if this option is enabled. | | EDITABLE_KEYS | editableKeys | core | true | Allow to edit key-pairs in the UI. |
| SELF_PROVISIONING | selfProvisioning | core | false | Allow registered users to automatically create peers via the RESTful API. | | CREATE_DEFAULT_PEER | createDefaultPeer | core | false | If an LDAP user logs in for the first time, a new WireGuard peer will be created on the WG_DEFAULT_DEVICE if this option is enabled. |
| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. | | SELF_PROVISIONING | selfProvisioning | core | false | Allow registered users to automatically create peers via the RESTful API. |
| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. | | WG_EXPORTER_FRIENDLY_NAMES | wgExporterFriendlyNames | core | false | Enable integration with [prometheus_wireguard_exporter friendly name](https://github.com/MindFlavor/prometheus_wireguard_exporter#friendly-tags). |
| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. | | LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. |
| DATABASE_HOST | host | database | | The mysql server address. | | SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. |
| DATABASE_PORT | port | database | | The mysql server port. | | DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. |
| DATABASE_NAME | database | database | data/wg_portal.db | For sqlite database: the database file-path, otherwise the database name. | | DATABASE_HOST | host | database | | The mysql server address. |
| DATABASE_USERNAME | user | database | | The mysql user. | | DATABASE_PORT | port | database | | The mysql server port. |
| DATABASE_PASSWORD | password | database | | The mysql password. | | DATABASE_NAME | database | database | data/wg_portal.db | For sqlite database: the database file-path, otherwise the database name. |
| EMAIL_HOST | host | email | 127.0.0.1 | The email server address. | | DATABASE_USERNAME | user | database | | The mysql user. |
| EMAIL_PORT | port | email | 25 | The email server port. | | DATABASE_PASSWORD | password | database | | The mysql password. |
| EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. | | EMAIL_HOST | host | email | 127.0.0.1 | The email server address. |
| EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. | | EMAIL_PORT | port | email | 25 | The email server port. |
| EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. | | EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. |
| EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. | | EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. |
| EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. | | EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. |
| EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. | | EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. |
| WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. | | EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. |
| WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). | | EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. |
| WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. | | WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. |
| MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. | | WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). |
| LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. | | WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. |
| LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. | | MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. |
| LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. | | LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. |
| LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. | | LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. |
| LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. | | LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. |
| LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. | | LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. |
| LDAP_LOGIN_FILTER | loginFilter | ldap | (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2)) | {{login_identifier}} will be replaced with the login email address. | | LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. |
| LDAP_SYNC_FILTER | syncFilter | ldap | (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*)) | The filter string for the LDAP synchronization service. | | LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. |
| LDAP_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. | | LDAP_LOGIN_FILTER | loginFilter | ldap | (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2)) | {{login_identifier}} will be replaced with the login email address. |
| LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. | | LDAP_SYNC_FILTER | syncFilter | ldap | (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*)) | The filter string for the LDAP synchronization service. |
| LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. | | LDAP_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. |
| LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. | | LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. |
| LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. | | LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. |
| LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. | | LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. |
| LOG_LEVEL | | | debug | Specify log level, one of: trace, debug, info, off. | | LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. |
| LOG_JSON | | | false | Format log output as JSON. | | LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. |
| LOG_COLOR | | | true | Colorize log output. | | LDAP_CERT_CONN | ldapCertConn | ldap | false | Allow connection with certificate against LDAP server without user/password |
| CONFIG_FILE | | | config.yml | The config file path. | | LDAPTLS_CERT | ldapTlsCert | ldap | | The LDAP cert's path |
| LDAPTLS_KEY | ldapTlsKey | ldap | | The LDAP key's path |
| LOG_LEVEL | | | debug | Specify log level, one of: trace, debug, info, off. |
| LOG_JSON | | | false | Format log output as JSON. |
| LOG_COLOR | | | true | Colorize log output. |
| CONFIG_FILE | | | config.yml | The config file path. |
### Sample yaml configuration ### Sample yaml configuration
config.yml: config.yml:
@@ -188,7 +193,7 @@ email:
user: test@gmail.com user: test@gmail.com
pass: topsecret pass: topsecret
wg: wg:
devices: devices:
- wg0 - wg0
- wg1 - wg1
defaultDevice: wg0 defaultDevice: wg0
@@ -197,16 +202,18 @@ wg:
``` ```
### RESTful API ### RESTful API
WireGuard Portal offers a RESTful API to interact with. WireGuard Portal offers a RESTful API to interact with.
The API is documented using OpenAPI 2.0, the Swagger UI can be found The API is documented using OpenAPI 2.0, the Swagger UI can be found
under the URL `http://<your wg-portal ip/domain>/swagger/index.html`. under the URL `http://<your wg-portal ip/domain>/swagger/index.html?displayOperationId=true`.
The [API's unittesting](tests/test_API.py) may serve as an example how to make use of the API with python3 & pyswagger.
## What is out of scope ## What is out of scope
* Creating or removing WireGuard (wgX) interfaces. * Creating or removing WireGuard (wgX) interfaces.
* Generation or application of any `iptables` or `nftables` rules. * Generation or application of any `iptables` or `nftables` rules.
* Setting up or changing IP-addresses of the WireGuard interface on operating systems other than linux. * Setting up or changing IP-addresses of the WireGuard interface on operating systems other than linux.
* Importing private keys of an existing WireGuard setup. * Importing private keys of an existing WireGuard setup.
## Application stack ## Application stack
* [Gin, HTTP web framework written in Go](https://github.com/gin-gonic/gin) * [Gin, HTTP web framework written in Go](https://github.com/gin-gonic/gin)
@@ -217,6 +224,6 @@ under the URL `http://<your wg-portal ip/domain>/swagger/index.html`.
## License ## License
* MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT * MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT
This project was inspired by [wg-gen-web](https://github.com/vx3r/wg-gen-web).
This project was inspired by [wg-gen-web](https://github.com/vx3r/wg-gen-web).

View File

@@ -2,4 +2,4 @@
* bootstrap-tokenfield * bootstrap-tokenfield
* https://github.com/sliptree/bootstrap-tokenfield * https://github.com/sliptree/bootstrap-tokenfield
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
*/@-webkit-keyframes 'blink'{0%{border-color:#ededed}100%{border-color:#b94a48}}@-moz-keyframes 'blink'{0%{border-color:#ededed}100%{border-color:#b94a48}}@keyframes 'blink'{0%{border-color:#ededed}100%{border-color:#b94a48}}.tokenfield{height:auto;min-height:34px;padding-bottom:0}.tokenfield.focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.tokenfield .token{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;display:inline-block;border:1px solid #d9d9d9;background-color:#ededed;white-space:nowrap;margin:-1px 5px 5px 0;height:22px;vertical-align:top;cursor:default}.tokenfield .token:hover{border-color:#b9b9b9}.tokenfield .token.active{border-color:#52a8ec;border-color:rgba(82,168,236,.8)}.tokenfield .token.duplicate{border-color:#ebccd1;-webkit-animation-name:blink;animation-name:blink;-webkit-animation-duration:.1s;animation-duration:.1s;-webkit-animation-direction:normal;animation-direction:normal;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.tokenfield .token.invalid{background:0 0;border:1px solid transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border-bottom:1px dotted #d9534f}.tokenfield .token.invalid.active{background:#ededed;border:1px solid #ededed;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.tokenfield .token .token-label{display:inline-block;overflow:hidden;text-overflow:ellipsis;padding-left:4px;vertical-align:top}.tokenfield .token .close{font-family:Arial;display:inline-block;line-height:100%;font-size:1.1em;line-height:1.49em;margin-left:5px;float:none;height:100%;vertical-align:top;padding-right:4px}.tokenfield .token-input{background:0 0;width:60px;min-width:60px;border:0;height:20px;padding:0;margin-bottom:6px;-webkit-box-shadow:none;box-shadow:none}.tokenfield .token-input:focus{border-color:transparent;outline:0;-webkit-box-shadow:none;box-shadow:none}.tokenfield.disabled{cursor:not-allowed;background-color:#eee}.tokenfield.disabled .token-input{cursor:not-allowed}.tokenfield.disabled .token:hover{cursor:not-allowed;border-color:#d9d9d9}.tokenfield.disabled .token:hover .close{cursor:not-allowed;opacity:.2;filter:alpha(opacity=20)}.has-warning .tokenfield.focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-error .tokenfield.focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-success .tokenfield.focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.tokenfield.input-sm,.input-group-sm .tokenfield{min-height:30px;padding-bottom:0}.input-group-sm .token,.tokenfield.input-sm .token{height:20px;margin-bottom:4px}.input-group-sm .token-input,.tokenfield.input-sm .token-input{height:18px;margin-bottom:5px}.tokenfield.input-lg,.input-group-lg .tokenfield{min-height:45px;padding-bottom:4px}.input-group-lg .token,.tokenfield.input-lg .token{height:25px}.input-group-lg .token-label,.tokenfield.input-lg .token-label{line-height:23px}.input-group-lg .token .close,.tokenfield.input-lg .token .close{line-height:1.3em}.input-group-lg .token-input,.tokenfield.input-lg .token-input{height:23px;line-height:23px;margin-bottom:6px;vertical-align:top}.tokenfield.rtl{direction:rtl;text-align:right}.tokenfield.rtl .token{margin:-1px 0 5px 5px}.tokenfield.rtl .token .token-label{padding-left:0;padding-right:4px} */@-webkit-keyframes blink{0%{border-color:#ededed}100%{border-color:#b94a48}}@-moz-keyframes blink{0%{border-color:#ededed}100%{border-color:#b94a48}}@keyframes blink{0%{border-color:#ededed}100%{border-color:#b94a48}}.tokenfield{height:auto;min-height:34px;padding-bottom:0}.tokenfield.focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.tokenfield .token{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;display:inline-block;border:1px solid #d9d9d9;background-color:#ededed;white-space:nowrap;margin:-1px 5px 5px 0;height:22px;vertical-align:top;cursor:default}.tokenfield .token:hover{border-color:#b9b9b9}.tokenfield .token.active{border-color:#52a8ec;border-color:rgba(82,168,236,.8)}.tokenfield .token.duplicate{border-color:#ebccd1;-webkit-animation-name:blink;animation-name:blink;-webkit-animation-duration:.1s;animation-duration:.1s;-webkit-animation-direction:normal;animation-direction:normal;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.tokenfield .token.invalid{background:0 0;border:1px solid transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border-bottom:1px dotted #d9534f}.tokenfield .token.invalid.active{background:#ededed;border:1px solid #ededed;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.tokenfield .token .token-label{display:inline-block;overflow:hidden;text-overflow:ellipsis;padding-left:4px;vertical-align:top}.tokenfield .token .close{font-family:Arial;display:inline-block;line-height:100%;font-size:1.1em;line-height:1.49em;margin-left:5px;float:none;height:100%;vertical-align:top;padding-right:4px}.tokenfield .token-input{background:0 0;width:60px;min-width:60px;border:0;height:20px;padding:0;margin-bottom:6px;-webkit-box-shadow:none;box-shadow:none}.tokenfield .token-input:focus{border-color:transparent;outline:0;-webkit-box-shadow:none;box-shadow:none}.tokenfield.disabled{cursor:not-allowed;background-color:#eee}.tokenfield.disabled .token-input{cursor:not-allowed}.tokenfield.disabled .token:hover{cursor:not-allowed;border-color:#d9d9d9}.tokenfield.disabled .token:hover .close{cursor:not-allowed;opacity:.2;filter:alpha(opacity=20)}.has-warning .tokenfield.focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-error .tokenfield.focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-success .tokenfield.focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.tokenfield.input-sm,.input-group-sm .tokenfield{min-height:30px;padding-bottom:0}.input-group-sm .token,.tokenfield.input-sm .token{height:20px;margin-bottom:4px}.input-group-sm .token-input,.tokenfield.input-sm .token-input{height:18px;margin-bottom:5px}.tokenfield.input-lg,.input-group-lg .tokenfield{height:auto;min-height:45px;padding-bottom:4px}.input-group-lg .token,.tokenfield.input-lg .token{height:25px}.input-group-lg .token-label,.tokenfield.input-lg .token-label{line-height:23px}.input-group-lg .token .close,.tokenfield.input-lg .token .close{line-height:1.3em}.input-group-lg .token-input,.tokenfield.input-lg .token-input{height:23px;line-height:23px;margin-bottom:6px;vertical-align:top}.tokenfield.rtl{direction:rtl;text-align:right}.tokenfield.rtl .token{margin:-1px 0 5px 5px}.tokenfield.rtl .token .token-label{padding-left:0;padding-right:4px}

View File

@@ -64,6 +64,11 @@ pre{background:#f7f7f9}iframe{overflow:hidden;border:none}@media (min-width: 768
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
} }
.navbar-brand > img {
height: 2rem;
width: auto;
}
.disabled-peer { .disabled-peer {
color: #d03131; color: #d03131;
} }

View File

@@ -1,3 +1,8 @@
.navbar { .navbar {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
}
.navbar-brand > img {
height: 2rem;
width: auto;
} }

13
assets/js/bootstrap-confirmation.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -30,6 +30,10 @@
this.form.submit(); this.form.submit();
}); });
}); });
$('[data-toggle=confirmation]').confirmation({
rootSelector: '[data-toggle=confirmation]',
// other options
});
})(jQuery); // End of use strict })(jQuery); // End of use strict

5
assets/js/popper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -17,7 +17,7 @@
{{template "prt_nav.html" .}} {{template "prt_nav.html" .}}
<div class="container mt-5"> <div class="container mt-5">
<h1>Create new clients</h1> <h1>Create new clients</h1>
<h2>Enter valid LDAP user email addresses to quickly create new accounts.</h2> <h2>Enter valid user email addresses to quickly create new accounts.</h2>
{{template "prt_flashes.html" .}} {{template "prt_flashes.html" .}}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}"> <input type="hidden" name="_csrf" value="{{.Csrf}}">
@@ -40,9 +40,10 @@
</div> </div>
{{template "prt_footer.html" .}} {{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery.easing.js"></script> <script src="/js/jquery.easing.js"></script>
<script src="/js/jquery-ui.min.js"></script> <script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/bootstrap-tokenfield.min.js"></script> <script src="/js/bootstrap-tokenfield.min.js"></script>
<script src="/js/custom.js"></script> <script src="/js/custom.js"></script>
<script>$('#inputEmail').on('tokenfield:createdtoken', function (e) { <script>$('#inputEmail').on('tokenfield:createdtoken', function (e) {
@@ -52,11 +53,19 @@
if (!valid) { if (!valid) {
$(e.relatedTarget).addClass('invalid') $(e.relatedTarget).addClass('invalid')
} }
}).on('tokenfield:createtoken', function (e) {
var existingTokens = $(this).tokenfield('getTokens');
$.each(existingTokens, function(index, token) {
if (token.value === e.attrs.value)
e.preventDefault();
});
}).tokenfield({ }).tokenfield({
autocomplete: { autocomplete: {
source: [{{range $i, $u :=.Users}}{{$u.Email}},{{end}}], source: [{{range $i, $u :=.Users}}{{if ne $i 0}},{{end}}'{{$u.Email}}'{{end}}],
delay: 100 delay: 100
}, },
inputType: 'email',
createTokensOnBlur: true,
showAutocompleteOnFocus: false showAutocompleteOnFocus: false
})</script> })</script>
</body> </body>

View File

@@ -203,8 +203,10 @@
</div> </div>
{{template "prt_footer.html" .}} {{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery.easing.js"></script> <script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script> <script src="/js/custom.js"></script>
</body> </body>

View File

@@ -28,7 +28,7 @@
<div id="configContent" class="tab-content"> <div id="configContent" class="tab-content">
<!-- server mode --> <!-- server mode -->
<div class="tab-pane fade {{if eq .Device.Type "server"}}active show{{end}}" id="server"> <div class="tab-pane fade {{if eq .Device.Type "server"}}active show{{end}}" id="server">
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data" name="server">
<input type="hidden" name="_csrf" value="{{.Csrf}}"> <input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="device" value="{{.Device.DeviceName}}"> <input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="devicetype" value="server"> <input type="hidden" name="devicetype" value="server">
@@ -162,7 +162,7 @@
<!-- client mode --> <!-- client mode -->
<div class="tab-pane fade {{if eq .Device.Type "client"}}active show{{end}}" id="client"> <div class="tab-pane fade {{if eq .Device.Type "client"}}active show{{end}}" id="client">
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data" name="client">
<input type="hidden" name="_csrf" value="{{.Csrf}}"> <input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="device" value="{{.Device.DeviceName}}"> <input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="devicetype" value="client"> <input type="hidden" name="devicetype" value="client">
@@ -253,8 +253,10 @@
</div> </div>
{{template "prt_footer.html" .}} {{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery.easing.js"></script> <script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script> <script src="/js/custom.js"></script>
</body> </body>

View File

@@ -11,78 +11,85 @@
</head> </head>
<body id="page-top" class="d-flex flex-column min-vh-100"> <body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}} {{template "prt_nav.html" .}}
<div class="container mt-5"> <div class="container mt-5">
{{if eq .User.CreatedAt .Epoch}}
<h1>Create a new user</h1>
{{else}}
<h1>Edit user <strong>{{.User.Email}}</strong></h1>
{{end}}
{{template "prt_flashes.html" .}}
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
{{if eq .User.CreatedAt .Epoch}} {{if eq .User.CreatedAt .Epoch}}
<div class="form-row"> <h1>Create a new user</h1>
<div class="form-group required col-md-12">
<label for="inputEmail">Email</label>
<input type="text" name="email" class="form-control" id="inputEmail" value="{{.User.Email}}" required>
</div>
</div>
{{else}} {{else}}
<input type="hidden" name="email" value="{{.User.Email}}"> <h1>Edit user <strong>{{.User.Email}}</strong></h1>
{{end}} {{end}}
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputFirstname">Firstname</label>
<input type="text" name="firstname" class="form-control" id="inputFirstname" value="{{.User.Firstname}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputLastname">Lastname</label>
<input type="text" name="lastname" class="form-control" id="inputLastname" value="{{.User.Lastname}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="inputPhone">Phone</label>
<input type="text" name="phone" class="form-control" id="inputPhone" value="{{.User.Phone}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12 {{if eq .User.CreatedAt .Epoch}}required{{end}}">
<label for="inputPassword">Password</label>
<input type="password" name="password" class="form-control" id="inputPassword" {{if eq .User.CreatedAt .Epoch}}required{{end}}>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isadmin" type="checkbox" value="true" id="inputAdmin" {{if .User.IsAdmin}}checked{{end}}>
<label class="custom-control-label" for="inputAdmin">
Administrator
</label>
</div>
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="inputDisabled" {{if .User.DeletedAt.Valid}}checked{{end}}>
<label class="custom-control-label" for="inputDisabled">
Disabled
</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button> {{template "prt_flashes.html" .}}
<a href="/admin/users/" class="btn btn-secondary">Cancel</a>
</form> <form method="post" enctype="multipart/form-data">
</div> <input type="hidden" name="_csrf" value="{{.Csrf}}">
{{template "prt_footer.html" .}} {{if eq .User.CreatedAt .Epoch}}
<script src="/js/jquery.min.js"></script> <div class="form-row">
<script src="/js/bootstrap.bundle.min.js"></script> <div class="form-group required col-md-12">
<script src="/js/jquery.easing.js"></script> <label for="inputEmail">Email</label>
<script src="/js/custom.js"></script> <input type="text" name="email" class="form-control" id="inputEmail" value="{{.User.Email}}" required>
</div>
</div>
{{else}}
<input type="hidden" name="email" value="{{.User.Email}}">
{{end}}
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputFirstname">Firstname</label>
<input type="text" name="firstname" class="form-control" id="inputFirstname" value="{{.User.Firstname}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputLastname">Lastname</label>
<input type="text" name="lastname" class="form-control" id="inputLastname" value="{{.User.Lastname}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="inputPhone">Phone</label>
<input type="text" name="phone" class="form-control" id="inputPhone" value="{{.User.Phone}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12 {{if eq .User.CreatedAt .Epoch}}required{{end}}">
<label for="inputPassword">Password</label>
<input type="password" name="password" class="form-control" id="inputPassword" {{if eq .User.CreatedAt .Epoch}}required{{end}}>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isadmin" type="checkbox" value="true" id="inputAdmin" {{if .User.IsAdmin}}checked{{end}}>
<label class="custom-control-label" for="inputAdmin">
Administrator
</label>
</div>
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="inputDisabled" {{if .User.DeletedAt.Valid}}checked{{end}}>
<label class="custom-control-label" for="inputDisabled">
Disabled
</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/admin/users/" class="btn btn-secondary">Cancel</a>
{{if eq $.Session.IsAdmin true}}
{{if eq .User.Source "db"}}
<a href="/admin/users/delete?pkey={{.User.Email}}" data-toggle="confirmation" data-title="Really delete user and associated peers?" title="Delete user and associated peers" class="btn btn-danger float-right">Delete</a>
{{end}}
{{end}}
</form>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body> </body>
</html> </html>

View File

@@ -125,7 +125,7 @@
</div> </div>
</div> </div>
<div class="mt-4 row"> <div class="mt-4 row">
<div class="col-sm-10 col-12"> <div class="col-sm-8 col-12">
{{if eq $.Device.Type "server"}} {{if eq $.Device.Type "server"}}
<h2 class="mt-2">Current VPN Peers</h2> <h2 class="mt-2">Current VPN Peers</h2>
{{end}} {{end}}
@@ -133,11 +133,12 @@
<h2 class="mt-2">Current VPN Endpoints</h2> <h2 class="mt-2">Current VPN Endpoints</h2>
{{end}} {{end}}
</div> </div>
<div class="col-sm-2 col-12 text-right"> <div class="col-sm-4 col-12 text-right">
<a href="/admin/peer/emailall" data-toggle="confirmation" data-title="Send mail to all peers?" title="Send mail to all peers" class="btn btn-light"><i class="fa fa-fw fa-paper-plane"></i></a>
{{if eq $.Device.Type "server"}} {{if eq $.Device.Type "server"}}
<a href="/admin/peer/createldap" title="Add multiple peers" class="btn btn-primary"><i class="fa fa-fw fa-user-plus"></i></a> <a href="/admin/peer/createldap" title="Add multiple peers" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-users"></i></a>
{{end}} {{end}}
<a href="/admin/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i>M</a> <a href="/admin/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-user"></i></a>
</div> </div>
</div> </div>
<div class="mt-2 table-responsive"> <div class="mt-2 table-responsive">
@@ -261,8 +262,10 @@
</div> </div>
{{template "prt_footer.html" .}} {{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery.easing.js"></script> <script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script> <script src="/js/custom.js"></script>
</body> </body>

View File

@@ -59,8 +59,10 @@
</div> </div>
{{template "prt_footer.html" .}} {{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery.easing.js"></script> <script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script> <script src="/js/custom.js"></script>
</body> </body>

View File

@@ -11,21 +11,23 @@
</head> </head>
<body id="page-top"> <body id="page-top">
{{template "prt_nav.html" .}} {{template "prt_nav.html" .}}
<div class="container"> <div class="container">
<div class="text-center mt-5"> <div class="text-center mt-5">
<div class="error mx-auto" data-text="{{.Data.Code}}"> <div class="error mx-auto" data-text="{{.Data.Code}}">
<p class="m-0">{{.Data.Code}}</p> <p class="m-0">{{.Data.Code}}</p>
</div>
<p class="text-dark mb-5 lead">{{.Data.Message}}</p>
<p class="text-black-50 mb-0">{{.Data.Details}}</p><a href="/">← Back to Dashboard</a>
</div> </div>
<p class="text-dark mb-5 lead">{{.Data.Message}}</p>
<p class="text-black-50 mb-0">{{.Data.Details}}</p><a href="/">← Back to Dashboard</a>
</div> </div>
</div> {{template "prt_footer.html" .}}
{{template "prt_footer.html" .}} <script src="/js/jquery.min.js"></script>
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.easing.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script> <script src="/js/popper.min.js"></script>
<script src="/js/jquery.easing.js"></script> <script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/custom.js"></script> <script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body> </body>
</html> </html>

View File

@@ -15,7 +15,7 @@
{{template "prt_nav.html" .}} {{template "prt_nav.html" .}}
<div class="container mt-2"> <div class="container mt-2">
<div class="page-header"> <div class="page-header">
<h1>WireGuard VPN Portal</h1> <h1>{{ .Static.WebsiteTitle }}</h1>
</div> </div>
{{template "prt_flashes.html" .}} {{template "prt_flashes.html" .}}
<p class="lead">WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN. </p> <p class="lead">WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN. </p>
@@ -79,8 +79,10 @@
</div> </div>
{{template "prt_footer.html" .}} {{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery.easing.js"></script> <script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script> <script src="/js/custom.js"></script>
</body> </body>

View File

@@ -27,11 +27,11 @@
<div class="card mt-5"> <div class="card mt-5">
<div class="card-header">Please sign in</div> <div class="card-header">Please sign in</div>
<div class="card-body"> <div class="card-body">
<form class="form-signin" method="post"> <form class="form-signin" method="post" name="login">
<input type="hidden" name="_csrf" value="{{.Csrf}}"> <input type="hidden" name="_csrf" value="{{.Csrf}}">
<div class="form-group"> <div class="form-group">
<label for="inputUsername">Email</label> <label for="inputUsername">Username</label>
<input type="text" name="username" class="form-control" id="inputUsername" aria-describedby="usernameHelp" placeholder="Enter email"> <input type="text" name="username" class="form-control" id="inputUsername" aria-describedby="usernameHelp" placeholder="Enter username or email">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="inputPassword">Password</label> <label for="inputPassword">Password</label>
@@ -56,8 +56,10 @@
{{template "prt_flashes.html" .}} {{template "prt_flashes.html" .}}
</div> </div>
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery.easing.js"></script> <script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script> <script src="/js/custom.js"></script>
</body> </body>

View File

@@ -102,8 +102,10 @@
</div> </div>
{{template "prt_footer.html" .}} {{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery.easing.js"></script> <script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script> <script src="/js/custom.js"></script>
</body> </body>

View File

@@ -4,6 +4,10 @@ services:
image: h44z/wg-portal:1.0.6 image: h44z/wg-portal:1.0.6
container_name: wg-portal container_name: wg-portal
restart: unless-stopped restart: unless-stopped
logging:
options:
max-size: "10m"
max-file: "3"
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
network_mode: "host" network_mode: "host"

26
go.mod
View File

@@ -7,11 +7,10 @@ require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/evanphx/json-patch v0.5.2 github.com/evanphx/json-patch v0.5.2
github.com/gin-contrib/sessions v0.0.3 github.com/gin-contrib/sessions v0.0.3
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.7.4
github.com/go-ldap/ldap/v3 v3.2.4 github.com/go-ldap/ldap/v3 v3.4.1
github.com/go-openapi/spec v0.20.3 // indirect
github.com/go-openapi/swag v0.19.15 // indirect github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/validator/v10 v10.4.1 github.com/go-playground/validator/v10 v10.9.0
github.com/gorilla/sessions v1.2.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect
github.com/kelseyhightower/envconfig v1.4.0 github.com/kelseyhightower/envconfig v1.4.0
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
@@ -19,19 +18,18 @@ require (
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/swaggo/gin-swagger v1.3.0 github.com/swaggo/gin-swagger v1.3.1
github.com/swaggo/swag v1.7.0 github.com/swaggo/swag v1.7.1
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
github.com/xhit/go-simple-mail/v2 v2.8.1 github.com/xhit/go-simple-mail/v2 v2.10.0
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 // indirect golang.org/x/tools v0.1.5 // indirect
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83 // indirect golang.zx2c4.com/wireguard v0.0.20200121 // indirect
golang.org/x/tools v0.1.0 // indirect golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gorm.io/driver/mysql v1.0.5 gorm.io/driver/mysql v1.1.2
gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.21.6 gorm.io/gorm v1.21.13
) )

346
go.sum Normal file
View File

@@ -0,0 +1,346 @@
git.prolicht.digital/pub/healthcheck v1.0.1 h1:cdNgcSyQL9oveFBC9V+XE4OVbfMEwqPqGdShH79sZ98=
git.prolicht.digital/pub/healthcheck v1.0.1/go.mod h1:5CVsGrijfedtLaYv3KJkfvM0nmzpgndC9MgBjC1tom4=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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=
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU=
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/docker/libcontainer v2.2.1+incompatible h1:++SbbkCw+X8vAd4j2gOCzZ2Nn7s2xFALTf7LZKmM1/0=
github.com/docker/libcontainer v2.2.1+incompatible/go.mod h1:osvj61pYsqhNCMLGX31xr7klUBhHb/ZBuXS0o1Fvwbw=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc=
github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
github.com/gin-contrib/sessions v0.0.0-20190101140330-dc5246754963/go.mod h1:4lkInX8nHSR62NSmhXM3xtPeMSyfiR58NaEz+om1lHM=
github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI=
github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU=
github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ=
github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A=
github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
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=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
github.com/mdlayher/netlink v1.4.0 h1:n3ARR+Fm0dDv37dj5wSWZXDKcy+U0zwcXS3zKMnSiT0=
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/milosgajdos/tenus v0.0.3 h1:jmaJzwaY1DUyYVD0lM4U+uvP2kkEg1VahDqRFxIkVBE=
github.com/milosgajdos/tenus v0.0.3/go.mod h1:eIjx29vNeDOYWJuCnaHY2r4fq5egetV26ry3on7p8qY=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438 h1:jnz/4VenymvySjE+Ez511s0pqVzkUOmr1fwCVytNNWk=
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
github.com/swaggo/gin-swagger v1.3.1 h1:mO9MU8O99WX+RM3jekzOV54g9Fo+Nbkk7rgrN1u9irM=
github.com/swaggo/gin-swagger v1.3.1/go.mod h1:Z6NtRBK2PRig0EUmy1Xu75CnCEs6vGYu9QZd/QWRYKU=
github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=
github.com/swaggo/swag v1.7.1 h1:gY9ZakXlNWg/i/v5bQBic7VMZ4teq4m89lpiao74p/s=
github.com/swaggo/swag v1.7.1/go.mod h1:gAiHxNTb9cIpNmA/VEGUP+CyZMCP/EW7mdtc8Bny+p8=
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e h1:nt2877sKfojlHCTOBXbpWjBkuWKritFaGIfgQwbQUls=
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e/go.mod h1:B4+Kq1u5FlULTjFSM707Q6e/cOHFv0z/6QRoxubDIQ8=
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f h1:oqdnd6OGlOUu1InG37hWcCB3a+Jy3fwjylyVboaNMwY=
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f/go.mod h1:X3Dd1SB8Gt1V968NTzpKFjMM6O8ccta2NPC6MprOxZQ=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.1.13 h1:nB3O5kBSQGjEQAcfe1aLUYuxmXdFKmYgBZhY32rQb6Q=
github.com/ugorji/go v1.1.13/go.mod h1:jxau1n+/wyTGLQoCkjok9r5zFa/FxT6eI5HiHKQszjc=
github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.1.13 h1:013LbFhocBoIqgHeIHKlV4JWYhqogATYWZhIcH0WHn4=
github.com/ugorji/go/codec v1.1.13/go.mod h1:oNVt3Dq+FO91WNQ/9JnHKQP2QJxTzoN7wCBFCq1OeuU=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca h1:lpvAjPK+PcxnbcB8H7axIb4fMNwjX9bE4DzwPjGg8aE=
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca/go.mod h1:XXKxNbpoLihvvT7orUZbs/iZayg1n4ip7iJakJPAwA8=
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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/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/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190611141213-3f473d35a33a/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-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
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/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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20210510120138-977fb7262007/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-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/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/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.5/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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/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.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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=
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
golang.zx2c4.com/wireguard v0.0.20200121 h1:vcswa5Q6f+sylDfjqyrVNNrjsFUUbPsgAQTBCAg/Qf8=
golang.zx2c4.com/wireguard v0.0.20200121/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.1.2 h1:OofcyE2lga734MxwcCW9uB4mWNXMr50uaGRVwQL2B0M=
gorm.io/driver/mysql v1.1.2/go.mod h1:4P/X9vSc3WTrhTLZ259cpFd6xKNYiSSdSZngkSBGIMM=
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
gorm.io/gorm v1.21.13 h1:JU5A4yVemRjdMndJ0oZU7VX+Nr2ICE3C60U5bgR6mHE=
gorm.io/gorm v1.21.13/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=

View File

@@ -2,6 +2,7 @@ package ldap
import ( import (
"crypto/tls" "crypto/tls"
"io/ioutil"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -154,23 +155,49 @@ func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authent
} }
func (provider Provider) open() (*ldap.Conn, error) { func (provider Provider) open() (*ldap.Conn, error) {
tlsConfig := &tls.Config{InsecureSkipVerify: !provider.config.CertValidation} var tlsConfig *tls.Config
if provider.config.LdapCertConn {
cert_plain, err := ioutil.ReadFile(provider.config.LdapTlsCert)
if err != nil {
return nil, errors.WithMessage(err, "failed to load the certificate")
}
key, err := ioutil.ReadFile(provider.config.LdapTlsKey)
if err != nil {
return nil, errors.WithMessage(err, "failed to load the key")
}
cert_x509, err := tls.X509KeyPair(cert_plain, key)
if err != nil {
return nil, errors.WithMessage(err, "failed X509")
}
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert_x509}}
} else {
tlsConfig = &tls.Config{InsecureSkipVerify: !provider.config.CertValidation}
}
conn, err := ldap.DialURL(provider.config.URL, ldap.DialWithTLSConfig(tlsConfig)) conn, err := ldap.DialURL(provider.config.URL, ldap.DialWithTLSConfig(tlsConfig))
if err != nil { if err != nil {
return nil, err return nil, errors.WithMessage(err, "failed to connect to LDAP")
} }
if provider.config.StartTLS { if provider.config.StartTLS {
// Reconnect with TLS // Reconnect with TLS
err = conn.StartTLS(tlsConfig) err = conn.StartTLS(tlsConfig)
if err != nil { if err != nil {
return nil, err return nil, errors.WithMessage(err, "failed to start TLS session")
} }
} }
err = conn.Bind(provider.config.BindUser, provider.config.BindPass) err = conn.Bind(provider.config.BindUser, provider.config.BindPass)
if err != nil { if err != nil {
return nil, err return nil, errors.WithMessage(err, "failed to bind user")
} }
return conn, nil return conn, nil

View File

@@ -1,5 +1,9 @@
package ldap package ldap
import (
gldap "github.com/go-ldap/ldap/v3"
)
type Type string type Type string
const ( const (
@@ -21,7 +25,11 @@ type Config struct {
PhoneAttribute string `yaml:"attrPhone" envconfig:"LDAP_ATTR_PHONE"` PhoneAttribute string `yaml:"attrPhone" envconfig:"LDAP_ATTR_PHONE"`
GroupMemberAttribute string `yaml:"attrGroups" envconfig:"LDAP_ATTR_GROUPS"` GroupMemberAttribute string `yaml:"attrGroups" envconfig:"LDAP_ATTR_GROUPS"`
LoginFilter string `yaml:"loginFilter" envconfig:"LDAP_LOGIN_FILTER"` // {{login_identifier}} gets replaced with the login email address LoginFilter string `yaml:"loginFilter" envconfig:"LDAP_LOGIN_FILTER"` // {{login_identifier}} gets replaced with the login email address
SyncFilter string `yaml:"syncFilter" envconfig:"LDAP_SYNC_FILTER"` SyncFilter string `yaml:"syncFilter" envconfig:"LDAP_SYNC_FILTER"`
AdminLdapGroup string `yaml:"adminGroup" envconfig:"LDAP_ADMIN_GROUP"` // Members of this group receive admin rights in WG-Portal AdminLdapGroup string `yaml:"adminGroup" envconfig:"LDAP_ADMIN_GROUP"` // Members of this group receive admin rights in WG-Portal
AdminLdapGroup_ *gldap.DN `yaml:"-"`
LdapCertConn bool `yaml:"ldapCertConn" envconfig:"LDAP_CERT_CONN"`
LdapTlsCert string `yaml:"ldapTlsCert" envconfig:"LDAPTLS_CERT"`
LdapTlsKey string `yaml:"ldapTlsKey" envconfig:"LDAPTLS_KEY"`
} }

View File

@@ -2,6 +2,7 @@ package ldap
import ( import (
"crypto/tls" "crypto/tls"
"io/ioutil"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -14,7 +15,33 @@ type RawLdapData struct {
} }
func Open(cfg *Config) (*ldap.Conn, error) { func Open(cfg *Config) (*ldap.Conn, error) {
tlsConfig := &tls.Config{InsecureSkipVerify: !cfg.CertValidation} var tlsConfig *tls.Config
if cfg.LdapCertConn {
cert_plain, err := ioutil.ReadFile(cfg.LdapTlsCert)
if err != nil {
return nil, errors.WithMessage(err, "failed to load the certificate")
}
key, err := ioutil.ReadFile(cfg.LdapTlsKey)
if err != nil {
return nil, errors.WithMessage(err, "failed to load the key")
}
cert_x509, err := tls.X509KeyPair(cert_plain, key)
if err != nil {
return nil, errors.WithMessage(err, "failed X509")
}
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert_x509}}
} else {
tlsConfig = &tls.Config{InsecureSkipVerify: !cfg.CertValidation}
}
conn, err := ldap.DialURL(cfg.URL, ldap.DialWithTLSConfig(tlsConfig)) conn, err := ldap.DialURL(cfg.URL, ldap.DialWithTLSConfig(tlsConfig))
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to connect to LDAP") return nil, errors.Wrap(err, "failed to connect to LDAP")

View File

@@ -50,6 +50,7 @@ type ApiError struct {
// GetUsers godoc // GetUsers godoc
// @Tags Users // @Tags Users
// @Summary Retrieves all users // @Summary Retrieves all users
// @ID GetUsers
// @Produce json // @Produce json
// @Success 200 {object} []users.User // @Success 200 {object} []users.User
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -66,8 +67,9 @@ func (s *ApiServer) GetUsers(c *gin.Context) {
// GetUser godoc // GetUser godoc
// @Tags Users // @Tags Users
// @Summary Retrieves user based on given Email // @Summary Retrieves user based on given Email
// @ID GetUser
// @Produce json // @Produce json
// @Param email query string true "User Email" // @Param Email query string true "User Email"
// @Success 200 {object} users.User // @Success 200 {object} users.User
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -76,9 +78,9 @@ func (s *ApiServer) GetUsers(c *gin.Context) {
// @Router /backend/user [get] // @Router /backend/user [get]
// @Security ApiBasicAuth // @Security ApiBasicAuth
func (s *ApiServer) GetUser(c *gin.Context) { func (s *ApiServer) GetUser(c *gin.Context) {
email := strings.ToLower(strings.TrimSpace(c.Query("email"))) email := strings.ToLower(strings.TrimSpace(c.Query("Email")))
if email == "" { if email == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "Email parameter must be specified"})
return return
} }
@@ -93,9 +95,10 @@ func (s *ApiServer) GetUser(c *gin.Context) {
// PostUser godoc // PostUser godoc
// @Tags Users // @Tags Users
// @Summary Creates a new user based on the given user model // @Summary Creates a new user based on the given user model
// @ID PostUser
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param user body users.User true "User Model" // @Param User body users.User true "User Model"
// @Success 200 {object} users.User // @Success 200 {object} users.User
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -106,7 +109,7 @@ func (s *ApiServer) GetUser(c *gin.Context) {
// @Security ApiBasicAuth // @Security ApiBasicAuth
func (s *ApiServer) PostUser(c *gin.Context) { func (s *ApiServer) PostUser(c *gin.Context) {
newUser := users.User{} newUser := users.User{}
if err := c.BindJSON(&newUser); err != nil { if err := c.ShouldBindJSON(&newUser); err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()}) c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return return
} }
@@ -132,10 +135,11 @@ func (s *ApiServer) PostUser(c *gin.Context) {
// PutUser godoc // PutUser godoc
// @Tags Users // @Tags Users
// @Summary Updates a user based on the given user model // @Summary Updates a user based on the given user model
// @ID PutUser
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param email query string true "User Email" // @Param Email query string true "User Email"
// @Param user body users.User true "User Model" // @Param User body users.User true "User Model"
// @Success 200 {object} users.User // @Success 200 {object} users.User
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -145,21 +149,21 @@ func (s *ApiServer) PostUser(c *gin.Context) {
// @Router /backend/user [put] // @Router /backend/user [put]
// @Security ApiBasicAuth // @Security ApiBasicAuth
func (s *ApiServer) PutUser(c *gin.Context) { func (s *ApiServer) PutUser(c *gin.Context) {
email := strings.ToLower(strings.TrimSpace(c.Query("email"))) email := strings.ToLower(strings.TrimSpace(c.Query("Email")))
if email == "" { if email == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "Email parameter must be specified"})
return return
} }
updateUser := users.User{} updateUser := users.User{}
if err := c.BindJSON(&updateUser); err != nil { if err := c.ShouldBindJSON(&updateUser); err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()}) c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return return
} }
// Changing email address is not allowed // Changing email address is not allowed
if email != updateUser.Email { if email != updateUser.Email {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must match the model email address"}) c.JSON(http.StatusBadRequest, ApiError{Message: "Email parameter must match the model email address"})
return return
} }
@@ -184,10 +188,11 @@ func (s *ApiServer) PutUser(c *gin.Context) {
// PatchUser godoc // PatchUser godoc
// @Tags Users // @Tags Users
// @Summary Updates a user based on the given partial user model // @Summary Updates a user based on the given partial user model
// @ID PatchUser
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param email query string true "User Email" // @Param Email query string true "User Email"
// @Param user body users.User true "User Model" // @Param User body users.User true "User Model"
// @Success 200 {object} users.User // @Success 200 {object} users.User
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -197,7 +202,7 @@ func (s *ApiServer) PutUser(c *gin.Context) {
// @Router /backend/user [patch] // @Router /backend/user [patch]
// @Security ApiBasicAuth // @Security ApiBasicAuth
func (s *ApiServer) PatchUser(c *gin.Context) { func (s *ApiServer) PatchUser(c *gin.Context) {
email := strings.ToLower(strings.TrimSpace(c.Query("email"))) email := strings.ToLower(strings.TrimSpace(c.Query("Email")))
if email == "" { if email == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
return return
@@ -250,8 +255,9 @@ func (s *ApiServer) PatchUser(c *gin.Context) {
// DeleteUser godoc // DeleteUser godoc
// @Tags Users // @Tags Users
// @Summary Deletes the specified user // @Summary Deletes the specified user
// @ID DeleteUser
// @Produce json // @Produce json
// @Param email query string true "User Email" // @Param Email query string true "User Email"
// @Success 204 "No content" // @Success 204 "No content"
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -261,7 +267,7 @@ func (s *ApiServer) PatchUser(c *gin.Context) {
// @Router /backend/user [delete] // @Router /backend/user [delete]
// @Security ApiBasicAuth // @Security ApiBasicAuth
func (s *ApiServer) DeleteUser(c *gin.Context) { func (s *ApiServer) DeleteUser(c *gin.Context) {
email := strings.ToLower(strings.TrimSpace(c.Query("email"))) email := strings.ToLower(strings.TrimSpace(c.Query("Email")))
if email == "" { if email == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
return return
@@ -284,8 +290,9 @@ func (s *ApiServer) DeleteUser(c *gin.Context) {
// GetPeers godoc // GetPeers godoc
// @Tags Peers // @Tags Peers
// @Summary Retrieves all peers for the given interface // @Summary Retrieves all peers for the given interface
// @ID GetPeers
// @Produce json // @Produce json
// @Param device query string true "Device Name" // @Param DeviceName query string true "Device Name"
// @Success 200 {object} []wireguard.Peer // @Success 200 {object} []wireguard.Peer
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError // @Failure 403 {object} ApiError
@@ -293,9 +300,9 @@ func (s *ApiServer) DeleteUser(c *gin.Context) {
// @Router /backend/peers [get] // @Router /backend/peers [get]
// @Security ApiBasicAuth // @Security ApiBasicAuth
func (s *ApiServer) GetPeers(c *gin.Context) { func (s *ApiServer) GetPeers(c *gin.Context) {
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device"))) deviceName := strings.ToLower(strings.TrimSpace(c.Query("DeviceName")))
if deviceName == "" { if deviceName == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "DeviceName parameter must be specified"})
return return
} }
@@ -312,8 +319,9 @@ func (s *ApiServer) GetPeers(c *gin.Context) {
// GetPeer godoc // GetPeer godoc
// @Tags Peers // @Tags Peers
// @Summary Retrieves the peer for the given public key // @Summary Retrieves the peer for the given public key
// @ID GetPeer
// @Produce json // @Produce json
// @Param pkey query string true "Public Key (Base 64)" // @Param PublicKey query string true "Public Key (Base 64)"
// @Success 200 {object} wireguard.Peer // @Success 200 {object} wireguard.Peer
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError // @Failure 403 {object} ApiError
@@ -321,9 +329,9 @@ func (s *ApiServer) GetPeers(c *gin.Context) {
// @Router /backend/peer [get] // @Router /backend/peer [get]
// @Security ApiBasicAuth // @Security ApiBasicAuth
func (s *ApiServer) GetPeer(c *gin.Context) { func (s *ApiServer) GetPeer(c *gin.Context) {
pkey := c.Query("pkey") pkey := c.Query("PublicKey")
if pkey == "" { if pkey == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "PublicKey parameter must be specified"})
return return
} }
@@ -338,10 +346,11 @@ func (s *ApiServer) GetPeer(c *gin.Context) {
// PostPeer godoc // PostPeer godoc
// @Tags Peers // @Tags Peers
// @Summary Creates a new peer based on the given peer model // @Summary Creates a new peer based on the given peer model
// @ID PostPeer
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param device query string true "Device Name" // @Param DeviceName query string true "Device Name"
// @Param peer body wireguard.Peer true "Peer Model" // @Param Peer body wireguard.Peer true "Peer Model"
// @Success 200 {object} wireguard.Peer // @Success 200 {object} wireguard.Peer
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -351,9 +360,9 @@ func (s *ApiServer) GetPeer(c *gin.Context) {
// @Router /backend/peers [post] // @Router /backend/peers [post]
// @Security ApiBasicAuth // @Security ApiBasicAuth
func (s *ApiServer) PostPeer(c *gin.Context) { func (s *ApiServer) PostPeer(c *gin.Context) {
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device"))) deviceName := strings.ToLower(strings.TrimSpace(c.Query("DeviceName")))
if deviceName == "" { if deviceName == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "DeviceName parameter must be specified"})
return return
} }
@@ -364,7 +373,7 @@ func (s *ApiServer) PostPeer(c *gin.Context) {
} }
newPeer := wireguard.Peer{} newPeer := wireguard.Peer{}
if err := c.BindJSON(&newPeer); err != nil { if err := c.ShouldBindJSON(&newPeer); err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()}) c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return return
} }
@@ -390,10 +399,11 @@ func (s *ApiServer) PostPeer(c *gin.Context) {
// PutPeer godoc // PutPeer godoc
// @Tags Peers // @Tags Peers
// @Summary Updates the given peer based on the given peer model // @Summary Updates the given peer based on the given peer model
// @ID PutPeer
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param pkey query string true "Public Key" // @Param PublicKey query string true "Public Key"
// @Param peer body wireguard.Peer true "Peer Model" // @Param Peer body wireguard.Peer true "Peer Model"
// @Success 200 {object} wireguard.Peer // @Success 200 {object} wireguard.Peer
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -404,14 +414,14 @@ func (s *ApiServer) PostPeer(c *gin.Context) {
// @Security ApiBasicAuth // @Security ApiBasicAuth
func (s *ApiServer) PutPeer(c *gin.Context) { func (s *ApiServer) PutPeer(c *gin.Context) {
updatePeer := wireguard.Peer{} updatePeer := wireguard.Peer{}
if err := c.BindJSON(&updatePeer); err != nil { if err := c.ShouldBindJSON(&updatePeer); err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()}) c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return return
} }
pkey := c.Query("pkey") pkey := c.Query("PublicKey")
if pkey == "" { if pkey == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "PublicKey parameter must be specified"})
return return
} }
@@ -422,7 +432,7 @@ func (s *ApiServer) PutPeer(c *gin.Context) {
// Changing public key is not allowed // Changing public key is not allowed
if pkey != updatePeer.PublicKey { if pkey != updatePeer.PublicKey {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must match the model public key"}) c.JSON(http.StatusBadRequest, ApiError{Message: "PublicKey parameter must match the model public key"})
return return
} }
@@ -446,10 +456,11 @@ func (s *ApiServer) PutPeer(c *gin.Context) {
// PatchPeer godoc // PatchPeer godoc
// @Tags Peers // @Tags Peers
// @Summary Updates the given peer based on the given partial peer model // @Summary Updates the given peer based on the given partial peer model
// @ID PatchPeer
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param pkey query string true "Public Key" // @Param PublicKey query string true "Public Key"
// @Param peer body wireguard.Peer true "Peer Model" // @Param Peer body wireguard.Peer true "Peer Model"
// @Success 200 {object} wireguard.Peer // @Success 200 {object} wireguard.Peer
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -465,7 +476,7 @@ func (s *ApiServer) PatchPeer(c *gin.Context) {
return return
} }
pkey := c.Query("pkey") pkey := c.Query("PublicKey")
if pkey == "" { if pkey == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
return return
@@ -498,7 +509,7 @@ func (s *ApiServer) PatchPeer(c *gin.Context) {
// Changing public key is not allowed // Changing public key is not allowed
if pkey != mergedPeer.PublicKey { if pkey != mergedPeer.PublicKey {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must match the model public key"}) c.JSON(http.StatusBadRequest, ApiError{Message: "PublicKey parameter must match the model public key"})
return return
} }
@@ -522,8 +533,9 @@ func (s *ApiServer) PatchPeer(c *gin.Context) {
// DeletePeer godoc // DeletePeer godoc
// @Tags Peers // @Tags Peers
// @Summary Updates the given peer based on the given partial peer model // @Summary Updates the given peer based on the given partial peer model
// @ID DeletePeer
// @Produce json // @Produce json
// @Param pkey query string true "Public Key" // @Param PublicKey query string true "Public Key"
// @Success 202 "No Content" // @Success 202 "No Content"
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -533,9 +545,9 @@ func (s *ApiServer) PatchPeer(c *gin.Context) {
// @Router /backend/peer [delete] // @Router /backend/peer [delete]
// @Security ApiBasicAuth // @Security ApiBasicAuth
func (s *ApiServer) DeletePeer(c *gin.Context) { func (s *ApiServer) DeletePeer(c *gin.Context) {
pkey := c.Query("pkey") pkey := c.Query("PublicKey")
if pkey == "" { if pkey == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "PublicKey parameter must be specified"})
return return
} }
@@ -556,6 +568,7 @@ func (s *ApiServer) DeletePeer(c *gin.Context) {
// GetDevices godoc // GetDevices godoc
// @Tags Interface // @Tags Interface
// @Summary Get all devices // @Summary Get all devices
// @ID GetDevices
// @Produce json // @Produce json
// @Success 200 {object} []wireguard.Device // @Success 200 {object} []wireguard.Device
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
@@ -580,8 +593,9 @@ func (s *ApiServer) GetDevices(c *gin.Context) {
// GetDevice godoc // GetDevice godoc
// @Tags Interface // @Tags Interface
// @Summary Get the given device // @Summary Get the given device
// @ID GetDevice
// @Produce json // @Produce json
// @Param device query string true "Device Name" // @Param DeviceName query string true "Device Name"
// @Success 200 {object} wireguard.Device // @Success 200 {object} wireguard.Device
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -590,9 +604,9 @@ func (s *ApiServer) GetDevices(c *gin.Context) {
// @Router /backend/device [get] // @Router /backend/device [get]
// @Security ApiBasicAuth // @Security ApiBasicAuth
func (s *ApiServer) GetDevice(c *gin.Context) { func (s *ApiServer) GetDevice(c *gin.Context) {
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device"))) deviceName := strings.ToLower(strings.TrimSpace(c.Query("DeviceName")))
if deviceName == "" { if deviceName == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "DeviceName parameter must be specified"})
return return
} }
@@ -614,10 +628,11 @@ func (s *ApiServer) GetDevice(c *gin.Context) {
// PutDevice godoc // PutDevice godoc
// @Tags Interface // @Tags Interface
// @Summary Updates the given device based on the given device model (UNIMPLEMENTED) // @Summary Updates the given device based on the given device model (UNIMPLEMENTED)
// @ID PutDevice
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param device query string true "Device Name" // @Param DeviceName query string true "Device Name"
// @Param body body wireguard.Device true "Device Model" // @Param Device body wireguard.Device true "Device Model"
// @Success 200 {object} wireguard.Device // @Success 200 {object} wireguard.Device
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -628,14 +643,14 @@ func (s *ApiServer) GetDevice(c *gin.Context) {
// @Security ApiBasicAuth // @Security ApiBasicAuth
func (s *ApiServer) PutDevice(c *gin.Context) { func (s *ApiServer) PutDevice(c *gin.Context) {
updateDevice := wireguard.Device{} updateDevice := wireguard.Device{}
if err := c.BindJSON(&updateDevice); err != nil { if err := c.ShouldBindJSON(&updateDevice); err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()}) c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return return
} }
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device"))) deviceName := strings.ToLower(strings.TrimSpace(c.Query("DeviceName")))
if deviceName == "" { if deviceName == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "DeviceName parameter must be specified"})
return return
} }
@@ -653,7 +668,7 @@ func (s *ApiServer) PutDevice(c *gin.Context) {
// Changing device name is not allowed // Changing device name is not allowed
if deviceName != updateDevice.DeviceName { if deviceName != updateDevice.DeviceName {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must match the model device name"}) c.JSON(http.StatusBadRequest, ApiError{Message: "DeviceName parameter must match the model device name"})
return return
} }
@@ -665,10 +680,11 @@ func (s *ApiServer) PutDevice(c *gin.Context) {
// PatchDevice godoc // PatchDevice godoc
// @Tags Interface // @Tags Interface
// @Summary Updates the given device based on the given partial device model (UNIMPLEMENTED) // @Summary Updates the given device based on the given partial device model (UNIMPLEMENTED)
// @ID PatchDevice
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param device query string true "Device Name" // @Param DeviceName query string true "Device Name"
// @Param body body wireguard.Device true "Device Model" // @Param Device body wireguard.Device true "Device Model"
// @Success 200 {object} wireguard.Device // @Success 200 {object} wireguard.Device
// @Failure 400 {object} ApiError // @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
@@ -684,9 +700,9 @@ func (s *ApiServer) PatchDevice(c *gin.Context) {
return return
} }
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device"))) deviceName := strings.ToLower(strings.TrimSpace(c.Query("DeviceName")))
if deviceName == "" { if deviceName == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "DeviceName parameter must be specified"})
return return
} }
@@ -723,7 +739,7 @@ func (s *ApiServer) PatchDevice(c *gin.Context) {
// Changing device name is not allowed // Changing device name is not allowed
if deviceName != mergedDevice.DeviceName { if deviceName != mergedDevice.DeviceName {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must match the model device name"}) c.JSON(http.StatusBadRequest, ApiError{Message: "DeviceName parameter must match the model device name"})
return return
} }
@@ -742,8 +758,9 @@ type PeerDeploymentInformation struct {
// GetPeerDeploymentInformation godoc // GetPeerDeploymentInformation godoc
// @Tags Provisioning // @Tags Provisioning
// @Summary Retrieves all active peers for the given email address // @Summary Retrieves all active peers for the given email address
// @ID GetPeerDeploymentInformation
// @Produce json // @Produce json
// @Param email query string true "Email Address" // @Param Email query string true "Email Address"
// @Success 200 {object} []PeerDeploymentInformation "All active WireGuard peers" // @Success 200 {object} []PeerDeploymentInformation "All active WireGuard peers"
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError // @Failure 403 {object} ApiError
@@ -751,9 +768,9 @@ type PeerDeploymentInformation struct {
// @Router /provisioning/peers [get] // @Router /provisioning/peers [get]
// @Security GeneralBasicAuth // @Security GeneralBasicAuth
func (s *ApiServer) GetPeerDeploymentInformation(c *gin.Context) { func (s *ApiServer) GetPeerDeploymentInformation(c *gin.Context) {
email := c.Query("email") email := c.Query("Email")
if email == "" { if email == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "Email parameter must be specified"})
return return
} }
@@ -792,8 +809,9 @@ func (s *ApiServer) GetPeerDeploymentInformation(c *gin.Context) {
// GetPeerDeploymentConfig godoc // GetPeerDeploymentConfig godoc
// @Tags Provisioning // @Tags Provisioning
// @Summary Retrieves the peer config for the given public key // @Summary Retrieves the peer config for the given public key
// @ID GetPeerDeploymentConfig
// @Produce plain // @Produce plain
// @Param pkey query string true "Public Key (Base 64)" // @Param PublicKey query string true "Public Key (Base 64)"
// @Success 200 {object} string "The WireGuard configuration file" // @Success 200 {object} string "The WireGuard configuration file"
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError // @Failure 403 {object} ApiError
@@ -801,9 +819,9 @@ func (s *ApiServer) GetPeerDeploymentInformation(c *gin.Context) {
// @Router /provisioning/peer [get] // @Router /provisioning/peer [get]
// @Security GeneralBasicAuth // @Security GeneralBasicAuth
func (s *ApiServer) GetPeerDeploymentConfig(c *gin.Context) { func (s *ApiServer) GetPeerDeploymentConfig(c *gin.Context) {
pkey := c.Query("pkey") pkey := c.Query("PublicKey")
if pkey == "" { if pkey == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"}) c.JSON(http.StatusBadRequest, ApiError{Message: "PublicKey parameter must be specified"})
return return
} }
@@ -849,9 +867,10 @@ type ProvisioningRequest struct {
// PostPeerDeploymentConfig godoc // PostPeerDeploymentConfig godoc
// @Tags Provisioning // @Tags Provisioning
// @Summary Creates the requested peer config and returns the config file // @Summary Creates the requested peer config and returns the config file
// @ID PostPeerDeploymentConfig
// @Accept json // @Accept json
// @Produce plain // @Produce plain
// @Param body body ProvisioningRequest true "Provisioning Request Model" // @Param ProvisioningRequest body ProvisioningRequest true "Provisioning Request Model"
// @Success 200 {object} string "The WireGuard configuration file" // @Success 200 {object} string "The WireGuard configuration file"
// @Failure 401 {object} ApiError // @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError // @Failure 403 {object} ApiError
@@ -860,7 +879,7 @@ type ProvisioningRequest struct {
// @Security GeneralBasicAuth // @Security GeneralBasicAuth
func (s *ApiServer) PostPeerDeploymentConfig(c *gin.Context) { func (s *ApiServer) PostPeerDeploymentConfig(c *gin.Context) {
req := ProvisioningRequest{} req := ProvisioningRequest{}
if err := c.BindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()}) c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return return
} }

View File

@@ -12,6 +12,8 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
gldap "github.com/go-ldap/ldap/v3"
) )
var ErrInvalidSpecification = errors.New("specification must be a struct pointer") var ErrInvalidSpecification = errors.New("specification must be a struct pointer")
@@ -65,8 +67,10 @@ type Config struct {
EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"` EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"`
CreateDefaultPeer bool `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"` CreateDefaultPeer bool `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"`
SelfProvisioningAllowed bool `yaml:"selfProvisioning" envconfig:"SELF_PROVISIONING"` SelfProvisioningAllowed bool `yaml:"selfProvisioning" envconfig:"SELF_PROVISIONING"`
WGExoprterFriendlyNames bool `yaml:"wgExporterFriendlyNames" envconfig:"WG_EXPORTER_FRIENDLY_NAMES"`
LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"` LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"` SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"`
LogoUrl string `yaml:"logoUrl" envconfig:"LOGO_URL"`
} `yaml:"core"` } `yaml:"core"`
Database common.DatabaseConfig `yaml:"database"` Database common.DatabaseConfig `yaml:"database"`
Email common.MailConfig `yaml:"email"` Email common.MailConfig `yaml:"email"`
@@ -81,12 +85,14 @@ func NewConfig() *Config {
cfg.Core.ListeningAddress = ":8123" cfg.Core.ListeningAddress = ":8123"
cfg.Core.Title = "WireGuard VPN" cfg.Core.Title = "WireGuard VPN"
cfg.Core.CompanyName = "WireGuard Portal" cfg.Core.CompanyName = "WireGuard Portal"
cfg.Core.LogoUrl = "/img/header-logo.png"
cfg.Core.ExternalUrl = "http://localhost:8123" cfg.Core.ExternalUrl = "http://localhost:8123"
cfg.Core.MailFrom = "WireGuard VPN <noreply@company.com>" cfg.Core.MailFrom = "WireGuard VPN <noreply@company.com>"
cfg.Core.AdminUser = "admin@wgportal.local" cfg.Core.AdminUser = "admin@wgportal.local"
cfg.Core.AdminPassword = "wgportal" cfg.Core.AdminPassword = "wgportal"
cfg.Core.LdapEnabled = false cfg.Core.LdapEnabled = false
cfg.Core.EditableKeys = true cfg.Core.EditableKeys = true
cfg.Core.WGExoprterFriendlyNames = false
cfg.Core.SessionSecret = "secret" cfg.Core.SessionSecret = "secret"
cfg.Database.Typ = "sqlite" cfg.Database.Typ = "sqlite"
@@ -128,6 +134,10 @@ func NewConfig() *Config {
if err != nil { if err != nil {
logrus.Warnf("unable to load environment config: %v", err) logrus.Warnf("unable to load environment config: %v", err)
} }
cfg.LDAP.AdminLdapGroup_, err = gldap.ParseDN(cfg.LDAP.AdminLdapGroup)
if err != nil {
logrus.Warnf("Parsing AdminLDAPGroup failed: %v", err)
}
if cfg.WG.ManageIPAddresses && runtime.GOOS != "linux" { if cfg.WG.ManageIPAddresses && runtime.GOOS != "linux" {
logrus.Warnf("managing IP addresses only works on linux, feature disabled...") logrus.Warnf("managing IP addresses only works on linux, feature disabled...")

View File

@@ -1,14 +1,13 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag // This file was generated by swaggo/swag
package docs package docs
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"strings" "strings"
"text/template"
"github.com/alecthomas/template"
"github.com/swaggo/swag" "github.com/swaggo/swag"
) )
@@ -16,7 +15,7 @@ var doc = `{
"schemes": {{ marshal .Schemes }}, "schemes": {{ marshal .Schemes }},
"swagger": "2.0", "swagger": "2.0",
"info": { "info": {
"description": "{{.Description}}", "description": "{{escape .Description}}",
"title": "{{.Title}}", "title": "{{.Title}}",
"contact": { "contact": {
"name": "WireGuard Portal Project", "name": "WireGuard Portal Project",
@@ -45,11 +44,12 @@ var doc = `{
"Interface" "Interface"
], ],
"summary": "Get the given device", "summary": "Get the given device",
"operationId": "GetDevice",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Device Name", "description": "Device Name",
"name": "device", "name": "DeviceName",
"in": "query", "in": "query",
"required": true "required": true
} }
@@ -103,17 +103,18 @@ var doc = `{
"Interface" "Interface"
], ],
"summary": "Updates the given device based on the given device model (UNIMPLEMENTED)", "summary": "Updates the given device based on the given device model (UNIMPLEMENTED)",
"operationId": "PutDevice",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Device Name", "description": "Device Name",
"name": "device", "name": "DeviceName",
"in": "query", "in": "query",
"required": true "required": true
}, },
{ {
"description": "Device Model", "description": "Device Model",
"name": "body", "name": "Device",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
@@ -176,17 +177,18 @@ var doc = `{
"Interface" "Interface"
], ],
"summary": "Updates the given device based on the given partial device model (UNIMPLEMENTED)", "summary": "Updates the given device based on the given partial device model (UNIMPLEMENTED)",
"operationId": "PatchDevice",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Device Name", "description": "Device Name",
"name": "device", "name": "DeviceName",
"in": "query", "in": "query",
"required": true "required": true
}, },
{ {
"description": "Device Model", "description": "Device Model",
"name": "body", "name": "Device",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
@@ -248,6 +250,7 @@ var doc = `{
"Interface" "Interface"
], ],
"summary": "Get all devices", "summary": "Get all devices",
"operationId": "GetDevices",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -299,11 +302,12 @@ var doc = `{
"Peers" "Peers"
], ],
"summary": "Retrieves the peer for the given public key", "summary": "Retrieves the peer for the given public key",
"operationId": "GetPeer",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Public Key (Base 64)", "description": "Public Key (Base 64)",
"name": "pkey", "name": "PublicKey",
"in": "query", "in": "query",
"required": true "required": true
} }
@@ -351,17 +355,18 @@ var doc = `{
"Peers" "Peers"
], ],
"summary": "Updates the given peer based on the given peer model", "summary": "Updates the given peer based on the given peer model",
"operationId": "PutPeer",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Public Key", "description": "Public Key",
"name": "pkey", "name": "PublicKey",
"in": "query", "in": "query",
"required": true "required": true
}, },
{ {
"description": "Peer Model", "description": "Peer Model",
"name": "peer", "name": "Peer",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
@@ -421,11 +426,12 @@ var doc = `{
"Peers" "Peers"
], ],
"summary": "Updates the given peer based on the given partial peer model", "summary": "Updates the given peer based on the given partial peer model",
"operationId": "DeletePeer",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Public Key", "description": "Public Key",
"name": "pkey", "name": "PublicKey",
"in": "query", "in": "query",
"required": true "required": true
} }
@@ -482,17 +488,18 @@ var doc = `{
"Peers" "Peers"
], ],
"summary": "Updates the given peer based on the given partial peer model", "summary": "Updates the given peer based on the given partial peer model",
"operationId": "PatchPeer",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Public Key", "description": "Public Key",
"name": "pkey", "name": "PublicKey",
"in": "query", "in": "query",
"required": true "required": true
}, },
{ {
"description": "Peer Model", "description": "Peer Model",
"name": "peer", "name": "Peer",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
@@ -554,11 +561,12 @@ var doc = `{
"Peers" "Peers"
], ],
"summary": "Retrieves all peers for the given interface", "summary": "Retrieves all peers for the given interface",
"operationId": "GetPeers",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Device Name", "description": "Device Name",
"name": "device", "name": "DeviceName",
"in": "query", "in": "query",
"required": true "required": true
} }
@@ -609,17 +617,18 @@ var doc = `{
"Peers" "Peers"
], ],
"summary": "Creates a new peer based on the given peer model", "summary": "Creates a new peer based on the given peer model",
"operationId": "PostPeer",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Device Name", "description": "Device Name",
"name": "device", "name": "DeviceName",
"in": "query", "in": "query",
"required": true "required": true
}, },
{ {
"description": "Peer Model", "description": "Peer Model",
"name": "peer", "name": "Peer",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
@@ -681,11 +690,12 @@ var doc = `{
"Users" "Users"
], ],
"summary": "Retrieves user based on given Email", "summary": "Retrieves user based on given Email",
"operationId": "GetUser",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "User Email", "description": "User Email",
"name": "email", "name": "Email",
"in": "query", "in": "query",
"required": true "required": true
} }
@@ -739,17 +749,18 @@ var doc = `{
"Users" "Users"
], ],
"summary": "Updates a user based on the given user model", "summary": "Updates a user based on the given user model",
"operationId": "PutUser",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "User Email", "description": "User Email",
"name": "email", "name": "Email",
"in": "query", "in": "query",
"required": true "required": true
}, },
{ {
"description": "User Model", "description": "User Model",
"name": "user", "name": "User",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
@@ -809,11 +820,12 @@ var doc = `{
"Users" "Users"
], ],
"summary": "Deletes the specified user", "summary": "Deletes the specified user",
"operationId": "DeleteUser",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "User Email", "description": "User Email",
"name": "email", "name": "Email",
"in": "query", "in": "query",
"required": true "required": true
} }
@@ -870,17 +882,18 @@ var doc = `{
"Users" "Users"
], ],
"summary": "Updates a user based on the given partial user model", "summary": "Updates a user based on the given partial user model",
"operationId": "PatchUser",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "User Email", "description": "User Email",
"name": "email", "name": "Email",
"in": "query", "in": "query",
"required": true "required": true
}, },
{ {
"description": "User Model", "description": "User Model",
"name": "user", "name": "User",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
@@ -942,6 +955,7 @@ var doc = `{
"Users" "Users"
], ],
"summary": "Retrieves all users", "summary": "Retrieves all users",
"operationId": "GetUsers",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -988,10 +1002,11 @@ var doc = `{
"Users" "Users"
], ],
"summary": "Creates a new user based on the given user model", "summary": "Creates a new user based on the given user model",
"operationId": "PostUser",
"parameters": [ "parameters": [
{ {
"description": "User Model", "description": "User Model",
"name": "user", "name": "User",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
@@ -1053,11 +1068,12 @@ var doc = `{
"Provisioning" "Provisioning"
], ],
"summary": "Retrieves the peer config for the given public key", "summary": "Retrieves the peer config for the given public key",
"operationId": "GetPeerDeploymentConfig",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Public Key (Base 64)", "description": "Public Key (Base 64)",
"name": "pkey", "name": "PublicKey",
"in": "query", "in": "query",
"required": true "required": true
} }
@@ -1104,11 +1120,12 @@ var doc = `{
"Provisioning" "Provisioning"
], ],
"summary": "Retrieves all active peers for the given email address", "summary": "Retrieves all active peers for the given email address",
"operationId": "GetPeerDeploymentInformation",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Email Address", "description": "Email Address",
"name": "email", "name": "Email",
"in": "query", "in": "query",
"required": true "required": true
} }
@@ -1159,10 +1176,11 @@ var doc = `{
"Provisioning" "Provisioning"
], ],
"summary": "Creates the requested peer config and returns the config file", "summary": "Creates the requested peer config and returns the config file",
"operationId": "PostPeerDeploymentConfig",
"parameters": [ "parameters": [
{ {
"description": "Provisioning Request Model", "description": "Provisioning Request Model",
"name": "body", "name": "ProvisioningRequest",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
@@ -1200,22 +1218,10 @@ var doc = `{
} }
}, },
"definitions": { "definitions": {
"gorm.DeletedAt": {
"type": "object",
"properties": {
"time": {
"type": "string"
},
"valid": {
"description": "Valid is true if Time is not NULL",
"type": "boolean"
}
}
},
"server.ApiError": { "server.ApiError": {
"type": "object", "type": "object",
"properties": { "properties": {
"message": { "Message": {
"type": "string" "type": "string"
} }
} }
@@ -1223,16 +1229,16 @@ var doc = `{
"server.PeerDeploymentInformation": { "server.PeerDeploymentInformation": {
"type": "object", "type": "object",
"properties": { "properties": {
"device": { "Device": {
"type": "string" "type": "string"
}, },
"deviceIdentifier": { "DeviceIdentifier": {
"type": "string" "type": "string"
}, },
"identifier": { "Identifier": {
"type": "string" "type": "string"
}, },
"publicKey": { "PublicKey": {
"type": "string" "type": "string"
} }
} }
@@ -1240,30 +1246,30 @@ var doc = `{
"server.ProvisioningRequest": { "server.ProvisioningRequest": {
"type": "object", "type": "object",
"required": [ "required": [
"email", "Email",
"identifier" "Identifier"
], ],
"properties": { "properties": {
"allowedIPsStr": { "AllowedIPsStr": {
"type": "string" "type": "string"
}, },
"deviceName": { "DNSStr": {
"type": "string"
},
"DeviceName": {
"description": "DeviceName is optional, if not specified, the configured default device will be used.", "description": "DeviceName is optional, if not specified, the configured default device will be used.",
"type": "string" "type": "string"
}, },
"dnsstr": { "Email": {
"type": "string" "type": "string"
}, },
"email": { "Identifier": {
"type": "string" "type": "string"
}, },
"identifier": { "Mtu": {
"type": "string"
},
"mtu": {
"type": "integer" "type": "integer"
}, },
"persistentKeepalive": { "PersistentKeepalive": {
"type": "integer" "type": "integer"
} }
} }
@@ -1271,43 +1277,43 @@ var doc = `{
"users.User": { "users.User": {
"type": "object", "type": "object",
"required": [ "required": [
"email", "Email",
"firstname", "Firstname",
"lastname" "Lastname"
], ],
"properties": { "properties": {
"createdAt": { "CreatedAt": {
"description": "database internal fields", "description": "database internal fields",
"type": "string" "type": "string"
}, },
"deletedAt": { "DeletedAt": {
"$ref": "#/definitions/gorm.DeletedAt" "type": "string"
}, },
"email": { "Email": {
"description": "required fields", "description": "required fields",
"type": "string" "type": "string"
}, },
"firstname": { "Firstname": {
"description": "optional fields", "description": "optional fields",
"type": "string" "type": "string"
}, },
"isAdmin": { "IsAdmin": {
"type": "boolean" "type": "boolean"
}, },
"lastname": { "Lastname": {
"type": "string" "type": "string"
}, },
"password": { "Password": {
"description": "optional, integrated password authentication", "description": "optional, integrated password authentication",
"type": "string" "type": "string"
}, },
"phone": { "Phone": {
"type": "string" "type": "string"
}, },
"source": { "Source": {
"type": "string" "type": "string"
}, },
"updatedAt": { "UpdatedAt": {
"type": "string" "type": "string"
} }
} }
@@ -1315,87 +1321,87 @@ var doc = `{
"wireguard.Device": { "wireguard.Device": {
"type": "object", "type": "object",
"required": [ "required": [
"deviceName", "DeviceName",
"ipsStr", "IPsStr",
"privateKey", "PrivateKey",
"publicKey", "PublicKey",
"type" "Type"
], ],
"properties": { "properties": {
"createdAt": { "CreatedAt": {
"type": "string" "type": "string"
}, },
"defaultAllowedIPsStr": { "DNSStr": {
"description": "comma separated list of IPs that are used in the client config file",
"type": "string"
},
"defaultEndpoint": {
"description": "Settings that are applied to all peer by default",
"type": "string"
},
"defaultPersistentKeepalive": {
"type": "integer"
},
"deviceName": {
"type": "string"
},
"displayName": {
"type": "string"
},
"dnsstr": {
"description": "comma separated list of the DNS servers of the client, wg-quick addition", "description": "comma separated list of the DNS servers of the client, wg-quick addition",
"type": "string" "type": "string"
}, },
"firewallMark": { "DefaultAllowedIPsStr": {
"description": "comma separated list of IPs that are used in the client config file",
"type": "string"
},
"DefaultEndpoint": {
"description": "Settings that are applied to all peer by default",
"type": "string"
},
"DefaultPersistentKeepalive": {
"type": "integer" "type": "integer"
}, },
"ipsStr": { "DeviceName": {
"type": "string"
},
"DisplayName": {
"type": "string"
},
"FirewallMark": {
"type": "integer"
},
"IPsStr": {
"description": "comma separated list of the IPs of the client, wg-quick addition", "description": "comma separated list of the IPs of the client, wg-quick addition",
"type": "string" "type": "string"
}, },
"listenPort": { "ListenPort": {
"type": "integer" "type": "integer"
}, },
"mtu": { "Mtu": {
"description": "the interface MTU, wg-quick addition", "description": "the interface MTU, wg-quick addition",
"type": "integer" "type": "integer"
}, },
"postDown": { "PostDown": {
"description": "post down script, wg-quick addition", "description": "post down script, wg-quick addition",
"type": "string" "type": "string"
}, },
"postUp": { "PostUp": {
"description": "post up script, wg-quick addition", "description": "post up script, wg-quick addition",
"type": "string" "type": "string"
}, },
"preDown": { "PreDown": {
"description": "pre down script, wg-quick addition", "description": "pre down script, wg-quick addition",
"type": "string" "type": "string"
}, },
"preUp": { "PreUp": {
"description": "pre up script, wg-quick addition", "description": "pre up script, wg-quick addition",
"type": "string" "type": "string"
}, },
"privateKey": { "PrivateKey": {
"description": "Core WireGuard Settings (Interface section)", "description": "Core WireGuard Settings (Interface section)",
"type": "string" "type": "string"
}, },
"publicKey": { "PublicKey": {
"description": "Misc. WireGuard Settings", "description": "Misc. WireGuard Settings",
"type": "string" "type": "string"
}, },
"routingTable": { "RoutingTable": {
"description": "the routing table, wg-quick addition", "description": "the routing table, wg-quick addition",
"type": "string" "type": "string"
}, },
"saveConfig": { "SaveConfig": {
"description": "if set to ` + "`" + `true', the configuration is saved from the current state of the interface upon shutdown, wg-quick addition", "description": "if set to ` + "`" + `true', the configuration is saved from the current state of the interface upon shutdown, wg-quick addition",
"type": "boolean" "type": "boolean"
}, },
"type": { "Type": {
"type": "string" "type": "string"
}, },
"updatedAt": { "UpdatedAt": {
"type": "string" "type": "string"
} }
} }
@@ -1403,71 +1409,84 @@ var doc = `{
"wireguard.Peer": { "wireguard.Peer": {
"type": "object", "type": "object",
"required": [ "required": [
"deviceName", "DeviceName",
"email", "DeviceType",
"identifier", "Email",
"publicKey" "Identifier",
"PublicKey",
"UID"
], ],
"properties": { "properties": {
"allowedIPsStr": { "AllowedIPsSrvStr": {
"description": "a comma separated list of IPs that are used in the server config file",
"type": "string"
},
"AllowedIPsStr": {
"description": "a comma separated list of IPs that are used in the client config file", "description": "a comma separated list of IPs that are used in the client config file",
"type": "string" "type": "string"
}, },
"createdAt": { "CreatedAt": {
"type": "string" "type": "string"
}, },
"createdBy": { "CreatedBy": {
"type": "string" "type": "string"
}, },
"deactivatedAt": { "DNSStr": {
"type": "string"
},
"deviceName": {
"type": "string"
},
"dnsstr": {
"description": "comma separated list of the DNS servers for the client", "description": "comma separated list of the DNS servers for the client",
"type": "string" "type": "string"
}, },
"email": { "DeactivatedAt": {
"type": "string" "type": "string"
}, },
"endpoint": { "DeviceName": {
"type": "string" "type": "string"
}, },
"identifier": { "DeviceType": {
"description": "Identifier AND Email make a WireGuard peer unique",
"type": "string" "type": "string"
}, },
"ignoreGlobalSettings": { "Email": {
"type": "boolean" "type": "string"
}, },
"ipsStr": { "Endpoint": {
"type": "string"
},
"IPsStr": {
"description": "a comma separated list of IPs of the client", "description": "a comma separated list of IPs of the client",
"type": "string" "type": "string"
}, },
"mtu": { "Identifier": {
"description": "Identifier AND Email make a WireGuard peer unique",
"type": "string"
},
"IgnoreGlobalSettings": {
"type": "boolean"
},
"Mtu": {
"description": "Global Device Settings (can be ignored, only make sense if device is in server mode)", "description": "Global Device Settings (can be ignored, only make sense if device is in server mode)",
"type": "integer" "type": "integer"
}, },
"persistentKeepalive": { "PersistentKeepalive": {
"type": "integer" "type": "integer"
}, },
"presharedKey": { "PresharedKey": {
"type": "string" "type": "string"
}, },
"privateKey": { "PrivateKey": {
"description": "Misc. WireGuard Settings", "description": "Misc. WireGuard Settings",
"type": "string" "type": "string"
}, },
"publicKey": { "PublicKey": {
"description": "Core WireGuard Settings", "description": "Core WireGuard Settings",
"type": "string" "type": "string"
}, },
"updatedAt": { "UID": {
"description": "uid for html identification",
"type": "string" "type": "string"
}, },
"updatedBy": { "UpdatedAt": {
"type": "string"
},
"UpdatedBy": {
"type": "string" "type": "string"
} }
} }
@@ -1513,6 +1532,13 @@ func (s *s) ReadDoc() string {
a, _ := json.Marshal(v) a, _ := json.Marshal(v)
return string(a) return string(a)
}, },
"escape": func(v interface{}) string {
// escape tabs
str := strings.Replace(v.(string), "\t", "\\t", -1)
// replace " with \", and if that results in \\", replace that with \\\"
str = strings.Replace(str, "\"", "\\\"", -1)
return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1)
},
}).Parse(doc) }).Parse(doc)
if err != nil { if err != nil {
return doc return doc

View File

@@ -192,3 +192,10 @@ func (s *Server) setFormInSession(c *gin.Context, formData interface{}) (Session
return currentSession, nil return currentSession, nil
} }
func (s *Server) isUserStillValid(email string) bool {
if s.users.GetUser(email) == nil {
return false
}
return true
}

View File

@@ -112,7 +112,7 @@ func (s *Server) GetInterfaceConfig(c *gin.Context) {
currentSession := GetSessionData(c) currentSession := GetSessionData(c)
device := s.peers.GetDevice(currentSession.DeviceName) device := s.peers.GetDevice(currentSession.DeviceName)
peers := s.peers.GetActivePeers(device.DeviceName) peers := s.peers.GetActivePeers(device.DeviceName)
cfg, err := device.GetConfigFile(peers) cfg, err := device.GetConfigFile(peers, s.config.Core.WGExoprterFriendlyNames)
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return return

View File

@@ -12,6 +12,7 @@ import (
"github.com/h44z/wg-portal/internal/common" "github.com/h44z/wg-portal/internal/common"
"github.com/h44z/wg-portal/internal/users" "github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard" "github.com/h44z/wg-portal/internal/wireguard"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/tatsushid/go-fastping" "github.com/tatsushid/go-fastping"
csrf "github.com/utrack/gin-csrf" csrf "github.com/utrack/gin-csrf"
@@ -235,7 +236,7 @@ func (s *Server) GetPeerConfig(c *gin.Context) {
return return
} }
cfg, err := peer.GetConfigFile(s.peers.GetDevice(currentSession.DeviceName)) cfg, err := peer.GetConfigFile(s.peers.GetDevice(peer.DeviceName))
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return return
@@ -254,59 +255,7 @@ func (s *Server) GetPeerConfigMail(c *gin.Context) {
return return
} }
user := s.users.GetUser(peer.Email) if err := s.sendPeerConfigMail(peer); err != nil {
cfg, err := peer.GetConfigFile(s.peers.GetDevice(currentSession.DeviceName))
if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return
}
png, err := peer.GetQRCode()
if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "QRCode error", err.Error())
return
}
// Apply mail template
qrcodeFileName := "wireguard-qrcode.png"
var tplBuff bytes.Buffer
if err := s.mailTpl.Execute(&tplBuff, struct {
Peer wireguard.Peer
User *users.User
QrcodePngName string
PortalUrl string
}{
Peer: peer,
User: user,
QrcodePngName: qrcodeFileName,
PortalUrl: s.config.Core.ExternalUrl,
}); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Template error", err.Error())
return
}
// Send mail
attachments := []common.MailAttachment{
{
Name: peer.GetConfigFileName(),
ContentType: "application/config",
Data: bytes.NewReader(cfg),
},
{
Name: qrcodeFileName,
ContentType: "image/png",
Data: bytes.NewReader(png),
Embedded: true,
},
{
Name: qrcodeFileName,
ContentType: "image/png",
Data: bytes.NewReader(png),
},
}
if err := common.SendEmailWithAttachments(s.config.Email, s.config.Core.MailFrom, "", "WireGuard VPN Configuration",
"Your mail client does not support HTML. Please find the configuration attached to this mail.", tplBuff.String(),
[]string{peer.Email}, attachments); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Email error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "Email error", err.Error())
return return
} }
@@ -367,3 +316,79 @@ func (s *Server) GetPeerStatus(c *gin.Context) {
c.JSON(http.StatusOK, isOnline) c.JSON(http.StatusOK, isOnline)
return return
} }
func (s *Server) GetAdminSendEmails(c *gin.Context) {
currentSession := GetSessionData(c)
if !currentSession.IsAdmin {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return
}
peers := s.peers.GetActivePeers(currentSession.DeviceName)
for _, peer := range peers {
if err := s.sendPeerConfigMail(peer); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Email error", err.Error())
return
}
}
SetFlashMessage(c, "emails sent successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin")
}
func (s *Server) sendPeerConfigMail(peer wireguard.Peer) error {
user := s.users.GetUser(peer.Email)
cfg, err := peer.GetConfigFile(s.peers.GetDevice(peer.DeviceName))
if err != nil {
return errors.Wrap(err, "failed to get config file")
}
png, err := peer.GetQRCode()
if err != nil {
return errors.Wrap(err, "failed to get qr-code")
}
// Apply mail template
qrcodeFileName := "wireguard-qrcode.png"
var tplBuff bytes.Buffer
if err := s.mailTpl.Execute(&tplBuff, struct {
Peer wireguard.Peer
User *users.User
QrcodePngName string
PortalUrl string
}{
Peer: peer,
User: user,
QrcodePngName: qrcodeFileName,
PortalUrl: s.config.Core.ExternalUrl,
}); err != nil {
return errors.Wrap(err, "failed to execute mail template")
}
// Send mail
attachments := []common.MailAttachment{
{
Name: peer.GetConfigFileName(),
ContentType: "application/config",
Data: bytes.NewReader(cfg),
},
{
Name: qrcodeFileName,
ContentType: "image/png",
Data: bytes.NewReader(png),
Embedded: true,
},
{
Name: qrcodeFileName,
ContentType: "image/png",
Data: bytes.NewReader(png),
},
}
if err := common.SendEmailWithAttachments(s.config.Email, s.config.Core.MailFrom, "", "WireGuard VPN Configuration",
"Your mail client does not support HTML. Please find the configuration attached to this mail.", tplBuff.String(),
[]string{peer.Email}, attachments); err != nil {
return errors.Wrap(err, "failed to send email")
}
return nil
}

View File

@@ -83,6 +83,26 @@ func (s *Server) GetAdminUsersEdit(c *gin.Context) {
}) })
} }
func (s *Server) GetAdminUsersDelete(c *gin.Context) {
user := s.users.GetUserUnscoped(c.Query("pkey"))
if user == nil {
SetFlashMessage(c, "invalid user", "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/")
return
}
urlEncodedKey := url.QueryEscape(c.Query("pkey"))
if err := s.HardDeleteUser(*user); err != nil {
SetFlashMessage(c, "failed to delete user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/edit?pkey="+urlEncodedKey+"&formerr=delete")
return
}
SetFlashMessage(c, "user deleted successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin/users/")
}
func (s *Server) PostAdminUsersEdit(c *gin.Context) { func (s *Server) PostAdminUsersEdit(c *gin.Context) {
currentUser := s.users.GetUserUnscoped(c.Query("pkey")) currentUser := s.users.GetUserUnscoped(c.Query("pkey"))
if currentUser == nil { if currentUser == nil {
@@ -113,7 +133,7 @@ func (s *Server) PostAdminUsersEdit(c *gin.Context) {
} else { } else {
formUser.DeletedAt = gorm.DeletedAt{} formUser.DeletedAt = gorm.DeletedAt{}
} }
formUser.IsAdmin = c.PostForm("isadmin") == "true" formUser.IsAdmin = c.PostForm("isadmin") != ""
if err := s.UpdateUser(formUser); err != nil { if err := s.UpdateUser(formUser); err != nil {
_ = s.updateFormInSession(c, formUser) _ = s.updateFormInSession(c, formUser)

View File

@@ -8,6 +8,8 @@ import (
"github.com/h44z/wg-portal/internal/users" "github.com/h44z/wg-portal/internal/users"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
gldap "github.com/go-ldap/ldap/v3"
) )
func (s *Server) SyncLdapWithUserDatabase() { func (s *Server) SyncLdapWithUserDatabase() {
@@ -31,6 +33,7 @@ func (s *Server) SyncLdapWithUserDatabase() {
logrus.Errorf("failed to fetch users from ldap: %v", err) logrus.Errorf("failed to fetch users from ldap: %v", err)
continue continue
} }
logrus.Tracef("found %d users in ldap", len(ldapUsers))
// Update existing LDAP users // Update existing LDAP users
s.updateLdapUsers(ldapUsers) s.updateLdapUsers(ldapUsers)
@@ -41,6 +44,19 @@ func (s *Server) SyncLdapWithUserDatabase() {
logrus.Info("ldap user synchronization stopped") logrus.Info("ldap user synchronization stopped")
} }
func (s Server) userIsInAdminGroup(ldapData *ldap.RawLdapData) bool {
if s.config.LDAP.AdminLdapGroup_ == nil {
return false
}
for _, group := range ldapData.RawAttributes[s.config.LDAP.GroupMemberAttribute] {
var dn, _ = gldap.ParseDN(string(group))
if s.config.LDAP.AdminLdapGroup_.Equal(dn) {
return true
}
}
return false
}
func (s Server) userChangedInLdap(user *users.User, ldapData *ldap.RawLdapData) bool { func (s Server) userChangedInLdap(user *users.User, ldapData *ldap.RawLdapData) bool {
if user.Firstname != ldapData.Attributes[s.config.LDAP.FirstNameAttribute] { if user.Firstname != ldapData.Attributes[s.config.LDAP.FirstNameAttribute] {
return true return true
@@ -62,14 +78,7 @@ func (s Server) userChangedInLdap(user *users.User, ldapData *ldap.RawLdapData)
return true return true
} }
ldapAdmin := false if user.IsAdmin != s.userIsInAdminGroup(ldapData) {
for _, group := range ldapData.RawAttributes[s.config.LDAP.GroupMemberAttribute] {
if string(group) == s.config.LDAP.AdminLdapGroup {
ldapAdmin = true
break
}
}
if user.IsAdmin != ldapAdmin {
return true return true
} }
@@ -105,7 +114,7 @@ func (s *Server) disableMissingLdapUsers(ldapUsers []ldap.RawLdapData) {
} }
} }
if err := s.users.DeleteUser(&activeUsers[i]); err != nil { if err := s.users.DeleteUser(&activeUsers[i], true); err != nil {
logrus.Errorf("failed to delete deactivated user %s in database: %v", activeUsers[i].Email, err) logrus.Errorf("failed to delete deactivated user %s in database: %v", activeUsers[i].Email, err)
} }
} }
@@ -142,17 +151,10 @@ func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData) {
user.Lastname = ldapUsers[i].Attributes[s.config.LDAP.LastNameAttribute] user.Lastname = ldapUsers[i].Attributes[s.config.LDAP.LastNameAttribute]
user.Email = ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute] user.Email = ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute]
user.Phone = ldapUsers[i].Attributes[s.config.LDAP.PhoneAttribute] user.Phone = ldapUsers[i].Attributes[s.config.LDAP.PhoneAttribute]
user.IsAdmin = false user.IsAdmin = s.userIsInAdminGroup(&ldapUsers[i])
user.Source = users.UserSourceLdap user.Source = users.UserSourceLdap
user.DeletedAt = gorm.DeletedAt{} // Not deleted user.DeletedAt = gorm.DeletedAt{} // Not deleted
for _, group := range ldapUsers[i].RawAttributes[s.config.LDAP.GroupMemberAttribute] {
if string(group) == s.config.LDAP.AdminLdapGroup {
user.IsAdmin = true
break
}
}
if err = s.users.UpdateUser(user); err != nil { if err = s.users.UpdateUser(user); err != nil {
logrus.Errorf("failed to update ldap user %s in database: %v", user.Email, err) logrus.Errorf("failed to update ldap user %s in database: %v", user.Email, err)
continue continue

View File

@@ -58,11 +58,13 @@ func SetupRoutes(s *Server) {
admin.GET("/peer/delete", s.GetAdminDeletePeer) admin.GET("/peer/delete", s.GetAdminDeletePeer)
admin.GET("/peer/download", s.GetPeerConfig) admin.GET("/peer/download", s.GetPeerConfig)
admin.GET("/peer/email", s.GetPeerConfigMail) admin.GET("/peer/email", s.GetPeerConfigMail)
admin.GET("/peer/emailall", s.GetAdminSendEmails)
admin.GET("/users/", s.GetAdminUsersIndex) admin.GET("/users/", s.GetAdminUsersIndex)
admin.GET("/users/create", s.GetAdminUsersCreate) admin.GET("/users/create", s.GetAdminUsersCreate)
admin.POST("/users/create", s.PostAdminUsersCreate) admin.POST("/users/create", s.PostAdminUsersCreate)
admin.GET("/users/edit", s.GetAdminUsersEdit) admin.GET("/users/edit", s.GetAdminUsersEdit)
admin.GET("/users/delete", s.GetAdminUsersDelete)
admin.POST("/users/edit", s.PostAdminUsersEdit) admin.POST("/users/edit", s.PostAdminUsersEdit)
// User routes // User routes
@@ -140,6 +142,14 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
return return
} }
// Check if logged-in user is still valid
if !s.isUserStillValid(session.Email) {
_ = DestroySessionData(c)
c.Abort()
s.GetHandleError(c, http.StatusUnauthorized, "unauthorized", "session no longer available")
return
}
// Continue down the chain to handler etc // Continue down the chain to handler etc
c.Next() c.Next()
} }

View File

@@ -116,7 +116,16 @@ func (s *Server) Setup(ctx context.Context) error {
s.server.Use(ginlogrus.Logger(logrus.StandardLogger())) s.server.Use(ginlogrus.Logger(logrus.StandardLogger()))
} }
s.server.Use(gin.Recovery()) s.server.Use(gin.Recovery())
s.server.Use(sessions.Sessions("authsession", memstore.NewStore([]byte(s.config.Core.SessionSecret))))
// Authentication cookies
cookieStore := memstore.NewStore([]byte(s.config.Core.SessionSecret))
cookieStore.Options(sessions.Options{
Path: "/",
MaxAge: 86400, // auth session is valid for 1 day
Secure: strings.HasPrefix(s.config.Core.ExternalUrl, "https"),
HttpOnly: true,
})
s.server.Use(sessions.Sessions("authsession", cookieStore))
s.server.SetFuncMap(template.FuncMap{ s.server.SetFuncMap(template.FuncMap{
"formatBytes": common.ByteCountSI, "formatBytes": common.ByteCountSI,
"urlEncode": url.QueryEscape, "urlEncode": url.QueryEscape,
@@ -244,7 +253,7 @@ func (s *Server) getExecutableDirectory() string {
func (s *Server) getStaticData() StaticData { func (s *Server) getStaticData() StaticData {
return StaticData{ return StaticData{
WebsiteTitle: s.config.Core.Title, WebsiteTitle: s.config.Core.Title,
WebsiteLogo: "/img/header-logo.png", WebsiteLogo: s.config.Core.LogoUrl,
CompanyName: s.config.Core.CompanyName, CompanyName: s.config.Core.CompanyName,
Year: time.Now().Year(), Year: time.Now().Year(),
Version: Version, Version: Version,

View File

@@ -103,16 +103,21 @@ func (s *Server) CreatePeer(device string, peer wireguard.Peer) error {
} }
peer.SetIPAddresses(peerIPs...) peer.SetIPAddresses(peerIPs...)
} }
if peer.PrivateKey == "" && dev.Type == wireguard.DeviceTypeServer { // if private key is empty create a new one if peer.PresharedKey == "" && dev.Type == wireguard.DeviceTypeServer { // if preshared key is empty create a new one
psk, err := wgtypes.GenerateKey() psk, err := wgtypes.GenerateKey()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to generate key") return errors.Wrap(err, "failed to generate key")
} }
peer.PresharedKey = psk.String()
}
if peer.PrivateKey == "" && peer.PublicKey == "" && dev.Type == wireguard.DeviceTypeServer { // if private key is empty create a new one
key, err := wgtypes.GeneratePrivateKey() key, err := wgtypes.GeneratePrivateKey()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to generate private key") return errors.Wrap(err, "failed to generate private key")
} }
peer.PresharedKey = psk.String()
peer.PrivateKey = key.String() peer.PrivateKey = key.String()
peer.PublicKey = key.PublicKey().String() peer.PublicKey = key.PublicKey().String()
} }
@@ -204,7 +209,7 @@ func (s *Server) WriteWireGuardConfigFile(device string) error {
} }
dev := s.peers.GetDevice(device) dev := s.peers.GetDevice(device)
cfg, err := dev.GetConfigFile(s.peers.GetActivePeers(device)) cfg, err := dev.GetConfigFile(s.peers.GetActivePeers(device), s.config.Core.WGExoprterFriendlyNames)
if err != nil { if err != nil {
return errors.WithMessage(err, "failed to get config file") return errors.WithMessage(err, "failed to get config file")
} }
@@ -248,10 +253,6 @@ func (s *Server) CreateUser(user users.User, device string) error {
// UpdateUser updates the user in the database. If the user is marked as deleted, it will get remove from the database. // UpdateUser updates the user in the database. If the user is marked as deleted, it will get remove from the database.
// Also, if the user is re-enabled, all it's linked WireGuard peers will be activated again. // Also, if the user is re-enabled, all it's linked WireGuard peers will be activated again.
func (s *Server) UpdateUser(user users.User) error { func (s *Server) UpdateUser(user users.User) error {
if user.DeletedAt.Valid {
return s.DeleteUser(user)
}
currentUser := s.users.GetUserUnscoped(user.Email) currentUser := s.users.GetUserUnscoped(user.Email)
// Hash user password (if set) // Hash user password (if set)
@@ -270,7 +271,12 @@ func (s *Server) UpdateUser(user users.User) error {
return errors.WithMessage(err, "failed to update user in manager") return errors.WithMessage(err, "failed to update user in manager")
} }
// If user was deleted (disabled), reactivate it's peers // Set to deleted (disabled) if user's deletedAt date is not empty
if user.DeletedAt.Valid {
return s.DeleteUser(user)
}
// Otherwise, if user was deleted (disabled), reactivate it's peers
if currentUser.DeletedAt.Valid { if currentUser.DeletedAt.Valid {
for _, peer := range s.peers.GetPeersByMail(user.Email) { for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now() now := time.Now()
@@ -284,24 +290,38 @@ func (s *Server) UpdateUser(user users.User) error {
return nil return nil
} }
// DeleteUser removes the user from the database. // DeleteUser soft-deletes the user from the database (disable the user).
// Also, if the user has linked WireGuard peers, they will be deactivated. // Also, if the user has linked WireGuard peers, they will be deactivated.
func (s *Server) DeleteUser(user users.User) error { func (s *Server) DeleteUser(user users.User) error {
currentUser := s.users.GetUserUnscoped(user.Email)
// Update in database // Update in database
if err := s.users.DeleteUser(&user); err != nil { if err := s.users.DeleteUser(&user, true); err != nil {
return errors.WithMessage(err, "failed to disable user in manager")
}
// Disable users peers
for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now()
peer.DeactivatedAt = &now
if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update deactivated peer %s for %s: %v", peer.PublicKey, user.Email, err)
}
}
return nil
}
// HardDeleteUser removes the user from the database.
// Also, if the user has linked WireGuard peers, they will be deleted.
func (s *Server) HardDeleteUser(user users.User) error {
// Update in database
if err := s.users.DeleteUser(&user, false); err != nil {
return errors.WithMessage(err, "failed to delete user in manager") return errors.WithMessage(err, "failed to delete user in manager")
} }
// If user was active, disable it's peers // remove all linked peers
if !currentUser.DeletedAt.Valid { for _, peer := range s.peers.GetPeersByMail(user.Email) {
for _, peer := range s.peers.GetPeersByMail(user.Email) { if err := s.DeletePeer(peer); err != nil {
now := time.Now() logrus.Errorf("failed to delete peer %s for %s: %v", peer.PublicKey, user.Email, err)
peer.DeactivatedAt = &now
if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update deactivated peer %s for %s: %v", peer.PublicKey, user.Email, err)
}
} }
} }
@@ -309,6 +329,11 @@ func (s *Server) DeleteUser(user users.User) error {
} }
func (s *Server) CreateUserDefaultPeer(email, device string) error { func (s *Server) CreateUserDefaultPeer(email, device string) error {
// Check if automatic peer creation is enabled
if !s.config.Core.CreateDefaultPeer {
return nil
}
// Check if user is active, if not, quit // Check if user is active, if not, quit
var existingUser *users.User var existingUser *users.User
if existingUser = s.users.GetUser(email); existingUser == nil { if existingUser = s.users.GetUser(email); existingUser == nil {
@@ -316,18 +341,26 @@ func (s *Server) CreateUserDefaultPeer(email, device string) error {
} }
// Check if user already has a peer setup, if not, create one // Check if user already has a peer setup, if not, create one
if s.config.Core.CreateDefaultPeer { peers := s.peers.GetPeersByMail(email)
peers := s.peers.GetPeersByMail(email) if len(peers) != 0 {
if len(peers) == 0 { // Create default vpn peer return nil
if err := s.CreatePeer(device, wireguard.Peer{ }
Identifier: existingUser.Firstname + " " + existingUser.Lastname + " (Default)",
Email: existingUser.Email, // Create default vpn peer
CreatedBy: existingUser.Email, peer, err := s.PrepareNewPeer(device)
UpdatedBy: existingUser.Email, if err != nil {
}); err != nil { return errors.WithMessage(err, "failed to prepare new peer")
return errors.WithMessagef(err, "failed to automatically create vpn peer for %s", email) }
} peer.Email = email
} if existingUser.Firstname != "" && existingUser.Lastname != "" {
peer.Identifier = fmt.Sprintf("%s %s (%s)", existingUser.Firstname, existingUser.Lastname, "Default")
} else {
peer.Identifier = fmt.Sprintf("%s (%s)", existingUser.Email, "Default")
}
peer.CreatedBy = existingUser.Email
peer.UpdatedBy = existingUser.Email
if err := s.CreatePeer(device, peer); err != nil {
return errors.WithMessagef(err, "failed to automatically create vpn peer for %s", email)
} }
return nil return nil

View File

@@ -161,9 +161,14 @@ func (m Manager) UpdateUser(user *User) error {
return nil return nil
} }
func (m Manager) DeleteUser(user *User) error { func (m Manager) DeleteUser(user *User, soft bool) error {
user.Email = strings.ToLower(user.Email) user.Email = strings.ToLower(user.Email)
res := m.db.Delete(user) var res *gorm.DB
if soft {
res = m.db.Delete(user)
} else {
res = m.db.Unscoped().Delete(user)
}
if res.Error != nil { if res.Error != nil {
return errors.Wrapf(res.Error, "failed to update user %s", user.Email) return errors.Wrapf(res.Error, "failed to update user %s", user.Email)
} }

View File

@@ -29,7 +29,7 @@ type User struct {
// required fields // required fields
Email string `gorm:"primaryKey" form:"email" binding:"required,email"` Email string `gorm:"primaryKey" form:"email" binding:"required,email"`
Source UserSource Source UserSource
IsAdmin bool IsAdmin bool `form:"isadmin"`
// optional fields // optional fields
Firstname string `form:"firstname" binding:"required"` Firstname string `form:"firstname" binding:"required"`
@@ -42,5 +42,5 @@ type User struct {
// database internal fields // database internal fields
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index" json:",omitempty"` DeletedAt gorm.DeletedAt `gorm:"index" json:",omitempty" swaggertype:"string"`
} }

View File

@@ -66,9 +66,9 @@ type Peer struct {
Peer *wgtypes.Peer `gorm:"-" json:"-"` // WireGuard peer Peer *wgtypes.Peer `gorm:"-" json:"-"` // WireGuard peer
Config string `gorm:"-" json:"-"` Config string `gorm:"-" json:"-"`
UID string `form:"uid" binding:"required,alphanum" json:"-"` // uid for html identification UID string `form:"uid" binding:"required,alphanum"` // uid for html identification
DeviceName string `gorm:"index" form:"device" binding:"required"` DeviceName string `gorm:"index" form:"device" binding:"required"`
DeviceType DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server" json:"-"` DeviceType DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server"`
Identifier string `form:"identifier" binding:"required,max=64"` // Identifier AND Email make a WireGuard peer unique Identifier string `form:"identifier" binding:"required,max=64"` // Identifier AND Email make a WireGuard peer unique
Email string `gorm:"index" form:"mail" binding:"required,email"` Email string `gorm:"index" form:"mail" binding:"required,email"`
IgnoreGlobalSettings bool `form:"ignoreglobalsettings"` IgnoreGlobalSettings bool `form:"ignoreglobalsettings"`
@@ -244,7 +244,7 @@ type Device struct {
Peers []Peer `gorm:"foreignKey:DeviceName" binding:"-" json:"-"` // linked WireGuard peers Peers []Peer `gorm:"foreignKey:DeviceName" binding:"-" json:"-"` // linked WireGuard peers
Type DeviceType `form:"devicetype" binding:"required,oneof=client server"` Type DeviceType `form:"devicetype" binding:"required,oneof=client server"`
DeviceName string `form:"device" gorm:"primaryKey" binding:"required,alphanum"` DeviceName string `form:"device" gorm:"primaryKey" binding:"required" validator:"regexp=[0-9a-zA-Z\-]+"`
DisplayName string `form:"displayname" binding:"omitempty,max=200"` DisplayName string `form:"displayname" binding:"omitempty,max=200"`
// Core WireGuard Settings (Interface section) // Core WireGuard Settings (Interface section)
@@ -338,12 +338,13 @@ func (d Device) GetConfig() wgtypes.Config {
return cfg return cfg
} }
func (d Device) GetConfigFile(peers []Peer) ([]byte, error) { func (d Device) GetConfigFile(peers []Peer, friendlyNames bool) ([]byte, error) {
var tplBuff bytes.Buffer var tplBuff bytes.Buffer
err := templateCache.ExecuteTemplate(&tplBuff, "interface.tpl", gin.H{ err := templateCache.ExecuteTemplate(&tplBuff, "interface.tpl", gin.H{
"Peers": peers, "Peers": peers,
"Interface": d, "Interface": d,
"FriendlyNames": friendlyNames,
}) })
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to execute server template") return nil, errors.Wrap(err, "failed to execute server template")

View File

@@ -56,6 +56,9 @@ PostDown = {{ .Interface.PostDown }}
# -WGP- PrivateKey: {{.PrivateKey}} # -WGP- PrivateKey: {{.PrivateKey}}
{{- end}} {{- end}}
[Peer] [Peer]
{{- if $.FriendlyNames}}
# friendly_name = {{ .Identifier }}
{{- end}}
PublicKey = {{ .PublicKey }} PublicKey = {{ .PublicKey }}
{{- if .PresharedKey}} {{- if .PresharedKey}}
PresharedKey = {{ .PresharedKey }} PresharedKey = {{ .PresharedKey }}
@@ -75,4 +78,4 @@ Endpoint = {{ .Endpoint }}
PersistentKeepalive = {{ .PersistentKeepalive }} PersistentKeepalive = {{ .PersistentKeepalive }}
{{- end}} {{- end}}
{{- end}} {{- end}}
{{end}} {{end}}

59
tests/README.md Normal file
View File

@@ -0,0 +1,59 @@
# pyswagger unittests for the API & UI
## Requirements
```
wg-quick up conf/wg-example0.conf
sudo LOG_LEVEL=debug CONFIG_FILE=conf/config.yml ../dist/wg-portal-amd64
python3 -m venv ~/venv/apitest
~/venv/apitest/bin/pip install pyswagger mechanize requests pytest PyYAML
```
## Running
### API
```
~/venv/apitest/bin/python3 -m unittest test_API.TestAPI
```
### UI
```
~/venv/lsl/bin/pytest pytest_UI.py
```
## Debugging
Debugging for requests http request/response is included for the API unittesting.
To use, adjust the log level for "api" logger to DEBUG
```python
log.setLevel(logging.DEBUG)
<action>
log.setLevel(logging.INFO)
```
This will provide:
```
2021-09-29 14:55:15,585 DEBUG api HTTP
---------------- request ----------------
GET http://localhost:8123/api/v1/provisioning/peers?Email=test%2Bn4gbm7%40example.org
User-Agent: python-requests/2.26.0
Accept-Encoding: gzip, deflate
Accept: application/json
Connection: keep-alive
Authorization: Basic d2dAZXhhbXBsZS5vcmc6YWJhZGNob2ljZQ==
None
---------------- response ----------------
200 OK http://localhost:8123/api/v1/provisioning/peers?Email=test%2Bn4gbm7%40example.org
Content-Type: application/json; charset=utf-8
Date: Wed, 29 Sep 2021 12:55:15 GMT
Content-Length: 285
[{"PublicKey":"hO3pxnft/8QL6nbE+79HN464Z+L4+D/JjUvNE+8LmTs=",
"Identifier":"Test User (Default)","Device":"wg-example0","DeviceIdentifier":"example0"},
{"PublicKey":"RVS2gsdRpFjyOpr1nAlEkrs194lQytaPHhaxL5amQxY=",
"Identifier":"debug","Device":"wg-example0","DeviceIdentifier":"example0"}]
```

27
tests/conf/config.yml Normal file
View File

@@ -0,0 +1,27 @@
core:
listeningAddress: :8123
externalUrl: https://wg.example.org
title: Example WireGuard VPN
company: Example.org
mailFrom: WireGuard VPN <noreply+wg@example.org>
logoUrl: /img/logo.png
adminUser: wg@example.org
adminPass: abadchoice
editableKeys: true
createDefaultPeer: true
selfProvisioning: true
ldapEnabled: false
database:
typ: sqlite
database: test.db
# :memory: does not work
email:
host: 127.0.0.1
port: 25
tls: false
wg:
devices:
- wg-example0
defaultDevice: wg-example0
configDirectory: /etc/wireguard
manageIPAddresses: true

View File

@@ -0,0 +1,16 @@
# AUTOGENERATED FILE - DO NOT EDIT
# -WGP- Interface: wg-example / Updated: 2021-09-27 08:52:05.537618409 +0000 UTC / Created: 2021-09-24 10:06:46.903674496 +0000 UTC
# -WGP- Interface display name: TheInterface
# -WGP- Interface mode: server
# -WGP- PublicKey = HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=
[Interface]
# Core settings
PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=
Address = 10.0.0.0/24
# Misc. settings (optional)
ListenPort = 51820
FwMark = 1
SaveConfig = true

214
tests/pytest_UI.py Normal file
View File

@@ -0,0 +1,214 @@
import logging.config
import http.cookiejar
import random
import string
import mechanize
import yaml
import pytest
@pytest.fixture(scope="function")
def browser():
# Fake Cookie Policy to send the Secure cookies via http
class InSecureCookiePolicy(http.cookiejar.DefaultCookiePolicy):
def set_ok(self, cookie, request):
return True
def return_ok(self, cookie, request):
return True
def domain_return_ok(self, domain, request):
return True
def path_return_ok(self, path, request):
return True
b = mechanize.Browser()
b.set_cookiejar(http.cookiejar.CookieJar(InSecureCookiePolicy()))
b.set_handle_robots(False)
b.set_debug_http(True)
return b
@pytest.fixture
def config():
cfg = yaml.load(open('conf/config.yml', 'r'))
return cfg
@pytest.fixture()
def admin(browser, config):
auth = (c := config['core'])['adminUser'], c['adminPass']
return _login(browser, auth)
def _create_user(admin, values):
b = admin
b.follow_link(text="User Management")
b.follow_link(predicate=has_attr('Add a user'))
# FIXME name form
b.select_form(predicate=lambda x: x.method == 'post')
for k, v in values.items():
b.form.set_value(v, k)
b.submit()
alert = b._factory.root.findall('body/div/div[@role="alert"]')
assert len(alert) == 1 and alert[0].text.strip() == "user created successfully"
return values["email"],values["password"]
def _destroy_user(admin, uid):
b = admin
b.follow_link(text="User Management")
for user in b._factory.root.findall('body/div/div/table[@id="userTable"]/tbody/'):
email,*_ = list(map(lambda x: x.text.strip() if x.text else '', list(user)))
if email == uid:
break
else:
assert False
a = user.findall('td/a[@title="Edit user"]')
assert len(a) == 1
b.follow_link(url=a[0].attrib['href'])
# FIXME name form
b.select_form(predicate=lambda x: x.method == 'post')
disabled = b.find_control("isdisabled")
disabled.set_single("true")
b.submit()
def _destroy_peer(admin, uid):
b = admin
b.follow_link(text="Administration")
peers = b._factory.root.findall('body/div/div/table[@id="userTable"]/tbody/tr')
for idx,peer in enumerate(peers):
if idx % 2 == 1:
continue
head, Identifier, PublicKey, EMail, IPs, Handshake, tail = list(map(lambda x: x.text.strip() if x.text else x, list(peer)))
print(Identifier)
if EMail != uid:
continue
peer = peers[idx+1]
a = peer.findall('.//a[@title="Delete peer"]')
assert len(a) == 1
b.follow_link(url=a[0].attrib['href'])
def _list_peers(user):
r = []
b = user
b.follow_link(predicate=has_attr('User-Profile'))
profiles = b._factory.root.findall('body/div/div/table[@id="userTable"]/tbody/tr')
for idx,profile in enumerate(profiles):
if idx % 2 == 1:
continue
head, Identifier, PublicKey, EMail, IPs, Handshake = list(map(lambda x: x.text.strip() if x.text else x, list(profile)))
profile = profiles[idx+1]
pre = profile.findall('.//pre')
assert len(pre) == 1
r.append((PublicKey, pre))
return r
@pytest.fixture(scope="session")
def user_data():
values = {
"email": f"test+{randstr()}@example.org",
"password": randstr(12),
"firstname": randstr(8),
"lastname": randstr(12)
}
return values
@pytest.fixture
def user(admin, user_data, config):
b = admin
auth = _create_user(b, user_data)
_logout(b)
_login(b, auth)
assert b.find_link(predicate=has_attr('User-Profile'))
yield b
_logout(b)
auth = (c := config['core'])['adminUser'], c['adminPass']
_login(b, auth)
_destroy_user(b, user_data["email"])
_destroy_peer(b, user_data["email"])
@pytest.fixture
def peer(admin, user, user_data):
pass
def _login(browser, auth):
b = browser
b.open("http://localhost:8123/")
b.follow_link(text="Login")
b.select_form(name="login")
username, password = auth
b.form.set_value(username, "username")
b.form.set_value(password, "password")
b.submit()
return b
def _logout(browser):
browser.follow_link(text="Logout")
return browser
def has_attr(value, attr='title'):
def find_attr(x):
return any([a == (attr, value) for a in x.attrs])
return find_attr
def _server(browser, addr):
b = browser
b.follow_link(text="Administration")
b.follow_link(predicate=has_attr('Edit interface settings'))
b.select_form("server")
values = {
"displayname": "example0",
"endpoint": "wg.example.org:51280",
"ip": addr
}
for k, v in values.items():
b.form.set_value(v, k)
b.submit()
return b
@pytest.fixture
def server(admin):
return _server(admin, "10.0.0.0/24")
def randstr(l=6):
return ''.join([random.choice(string.ascii_lowercase + string.digits) for i in range(l)])
def test_admin_login(admin):
b = admin
b.find_link("Administration")
def test_admin_server(admin):
ip = "10.0.0.0/28"
b = _server(admin, ip)
b.select_form("server")
assert ip == b.form.get_value("ip")
def test_admin_create_peer(server, user_data):
auth = _create_user(server, user_data)
def test_admin_create_user(admin, user_data):
auth = _create_user(admin, user_data)
def test_user_login(server, user):
b = user
b.follow_link(predicate=has_attr('User-Profile'))
def test_user_config(server, user):
b = user
peers = _list_peers(b)
assert len(peers) >= 1

484
tests/test_API.py Normal file
View File

@@ -0,0 +1,484 @@
import ipaddress
import collections
import string
import unittest
import datetime
import re
import uuid
import subprocess
import random
import logging
import logging.config
import mechanize
from pyswagger import App, Security
from pyswagger.contrib.client.requests import Client
log = logging.getLogger("api")
class HttpFormatter(logging.Formatter):
def _formatHeaders(self, d):
return '\n'.join(f'{k}: {v}' for k, v in d.items())
def formatMessage(self, record):
result = super().formatMessage(record)
if record.name == 'api':
result += '''
---------------- request ----------------
{req.method} {req.url}
{reqhdrs}
{req.body}
---------------- response ----------------
{res.status_code} {res.reason} {res.url}
{reshdrs}
{res.text}
---------------- end ----------------
'''.format(req=record.req, res=record.res, reqhdrs=self._formatHeaders(record.req.headers),
reshdrs=self._formatHeaders(record.res.headers), )
return result
logging.config.dictConfig(
{
"version": 1,
"formatters": {
"http": {
"()": HttpFormatter,
"format": "{asctime} {levelname} {name} {message}",
"style":'{',
},
"detailed": {
"class": "logging.Formatter",
"format": "%(asctime)s %(name)-9s %(levelname)-4s %(message)s",
},
"plain": {
"class": "logging.Formatter",
"format": "%(message)s",
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "detailed",
},
"console_http": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "http",
},
},
"root": {
"level": "DEBUG",
"handlers": ["console"],
"propagate": True
},
'loggers': {
'api': {
"level": "INFO",
"handlers": ["console_http"]
},
"requests.packages.urllib3": {
"level": "DEBUG",
"handlers": ["console"],
"propagate": True
},
},
}
)
log = logging.getLogger("api")
class ApiError(Exception):
pass
def logHttp(response, *args, **kwargs):
extra = {'req': response.request, 'res': response}
log.debug('HTTP', extra=extra)
class WGPClient:
def __init__(self, url, *auths):
app = App._create_(url)
auth = Security(app)
for t, cred in auths:
auth.update_with(t, cred)
client = Client(auth)
self.app, self.client = app, client
self.client._Client__s.hooks['response'] = logHttp
def call(self, name, **kwargs):
# print(f"{name} {kwargs}")
op = self.app.op[name]
req, resp = op(**kwargs)
now = datetime.datetime.now()
resp = self.client.request((req, resp))
then = datetime.datetime.now()
delta = then - now
# print(f"{resp.status} {delta}")
if 200 <= resp.status <= 299:
pass
elif 400 <= resp.status <= 499:
raise ApiError(resp.data["Message"])
elif 500 == resp.status:
raise ValueError(resp.data["Message"])
elif 501 == resp.status:
raise NotImplementedError(name)
elif 502 <= resp.status <= 599:
raise ApiError(resp.data["Message"])
return resp
def GetDevice(self, **kwargs):
return self.call("GetDevice", **kwargs).data
def PatchDevice(self, **kwargs):
return self.call("PatchDevice", **kwargs).data
def PutDevice(self, **kwargs):
return self.call("PutDevice", **kwargs).data
def GetDevices(self, **kwargs):
# FIXME - could return empty list?
return self.call("GetDevices", **kwargs).data or []
def DeletePeer(self, **kwargs):
return self.call("DeletePeer", **kwargs).data
def GetPeer(self, **kwargs):
return self.call("GetPeer", **kwargs).data
def PatchPeer(self, **kwargs):
return self.call("PatchPeer", **kwargs).data
def PostPeer(self, **kwargs):
return self.call("PostPeer", **kwargs).data
def PutPeer(self, **kwargs):
return self.call("PutPeer", **kwargs).data
def GetPeerDeploymentConfig(self, **kwargs):
return self.call("GetPeerDeploymentConfig", **kwargs).data
def PostPeerDeploymentConfig(self, **kwargs):
return self.call("PostPeerDeploymentConfig", **kwargs).raw
def GetPeerDeploymentInformation(self, **kwargs):
return self.call("GetPeerDeploymentInformation", **kwargs).data
def GetPeers(self, **kwargs):
return self.call("GetPeers", **kwargs).data
def DeleteUser(self, **kwargs):
return self.call("DeleteUser", **kwargs).data
def GetUser(self, **kwargs):
return self.call("GetUser", **kwargs).data
def PatchUser(self, **kwargs):
return self.call("PatchUser", **kwargs).data
def PostUser(self, **kwargs):
return self.call("PostUser", **kwargs).data
def PutUser(self, **kwargs):
return self.call("PutUser", **kwargs).data
def GetUsers(self, **kwargs):
return self.call("GetUsers", **kwargs).data
def generate_wireguard_keys():
"""
Generate a WireGuard private & public key
Requires that the 'wg' command is available on PATH
Returns (private_key, public_key), both strings
"""
privkey = subprocess.check_output("wg genkey", shell=True).decode("utf-8").strip()
pubkey = subprocess.check_output(f"echo '{privkey}' | wg pubkey", shell=True).decode("utf-8").strip()
return (privkey, pubkey)
KeyTuple = collections.namedtuple("Keys", "private public")
class TestAPI(unittest.TestCase):
URL = 'http://localhost:8123/swagger/doc.json'
AUTH = {
"api": ('ApiBasicAuth', ("wg@example.org", "abadchoice")),
"general": ('GeneralBasicAuth', ("wg@example.org", "abadchoice"))
}
DEVICE = "wg-example0"
IFADDR = "10.17.0.0/24"
log = logging.getLogger("TestAPI")
def _client(self, *auth):
auth = ["general"] if auth is None else auth
self.c = WGPClient(self.URL, *[self.AUTH[i] for i in auth])
@property
def randmail(self):
return 'test+' + ''.join(
[random.choice(string.ascii_lowercase + string.digits) for i in range(6)]) + '@example.org'
@classmethod
def setUpClass(cls) -> None:
cls.finishInstallation()
@classmethod
def finishInstallation(cls) -> None:
import http.cookiejar
# Fake Cookie Policy to send the Secure cookies via http
class InSecureCookiePolicy(http.cookiejar.DefaultCookiePolicy):
def set_ok(self, cookie, request):
return True
def return_ok(self, cookie, request):
return True
def domain_return_ok(self, domain, request):
return True
def path_return_ok(self, path, request):
return True
b = mechanize.Browser()
b.set_cookiejar(http.cookiejar.CookieJar(InSecureCookiePolicy()))
b.set_handle_robots(False)
b.open("http://localhost:8123/")
b.follow_link(text="Login")
b.select_form(name="login")
username, password = cls.AUTH['api'][1]
b.form.set_value(username, "username")
b.form.set_value(password, "password")
b.submit()
b.follow_link(text="Administration")
b.follow_link(predicate=lambda x: any([a == ('title', 'Edit interface settings') for a in x.attrs]))
b.select_form("server")
values = {
"displayname": "example0",
"endpoint": "wg.example.org:51280",
"ip": cls.IFADDR
}
for k, v in values.items():
b.form.set_value(v, k)
b.submit()
b.select_form("server")
# cls.log.debug(b.form.get_value("ip"))
def setUp(self) -> None:
self._client('api')
self.user = self.randmail
# create a user …
self.c.PostUser(User={"Firstname": "Test", "Lastname": "User", "Email": self.user})
self.keys = KeyTuple(*generate_wireguard_keys())
def _test_generate(self):
def key_of(op):
a, *b = list(filter(lambda x: len(x), re.split("([A-Z][a-z]+)", op.operationId)))
return ''.join(b), a
for op in sorted(self.c.app.op.values(), key=key_of):
print(f"""
def {op.operationId}(self, **kwargs):
return self. call("{op.operationId}", **kwargs)
""")
def test_ops(self):
for op in sorted(self.c.app.op.values(), key=lambda op: op.operationId):
self.assertTrue(hasattr(self.c, op.operationId), f"{op.operationId} is missing")
def test_Device(self):
# FIXME device has to be completed via webif to be valid before it can be used via API
devices = self.c.GetDevices()
self.assertTrue(len(devices) > 0)
for device in devices:
dev = self.c.GetDevice(DeviceName=device.DeviceName)
with self.assertRaises(NotImplementedError):
new = self.c.PutDevice(DeviceName=dev.DeviceName,
Device={
"DeviceName": dev.DeviceName,
"IPsStr": dev.IPsStr,
"PrivateKey": dev.PrivateKey,
"Type": "client",
"PublicKey": dev.PublicKey}
)
with self.assertRaises(NotImplementedError):
new = self.c.PatchDevice(DeviceName=dev.DeviceName,
Device={
"DeviceName": dev.DeviceName,
"IPsStr": dev.IPsStr,
"PrivateKey": dev.PrivateKey,
"Type": "client",
"PublicKey": dev.PublicKey}
)
break
def easy_peer(self):
data = self.c.PostPeerDeploymentConfig(ProvisioningRequest={"Email": self.user, "Identifier": "debug"})
data = data.decode()
pubkey = re.search("# -WGP- PublicKey: (?P<pubkey>[^\n]+)\n", data, re.MULTILINE)['pubkey']
privkey = re.search("PrivateKey = (?P<key>[^\n]+)\n", data, re.MULTILINE)['key']
self.keys = KeyTuple(privkey, pubkey)
def test_Peers(self):
privkey, pubkey = generate_wireguard_keys()
peer = {"UID": uuid.uuid4().hex,
"Identifier": uuid.uuid4().hex,
"DeviceName": self.DEVICE,
"PublicKey": pubkey,
"DeviceType": "client",
"IPsStr": str(self.IFADDR),
"Email": self.user}
# keypair is created server side if private key is not submitted
with self.assertRaisesRegex(ApiError, "peer not found"):
self.c.PostPeer(DeviceName=self.DEVICE, Peer=peer)
# create
peer["PrivateKey"] = privkey
p = self.c.PostPeer(DeviceName=self.DEVICE, Peer=peer)
self.assertListEqual([p.PrivateKey, p.PublicKey], [privkey, pubkey])
# lookup created peer
for p in self.c.GetPeers(DeviceName=self.DEVICE):
if pubkey == p.PublicKey:
break
else:
self.assertTrue(False)
# get
gp = self.c.GetPeer(PublicKey=p.PublicKey)
self.assertListEqual([gp.PrivateKey, gp.PublicKey], [p.PrivateKey, p.PublicKey])
# change?
peer['Identifier'] = 'changed'
n = self.c.PatchPeer(PublicKey=p.PublicKey, Peer=peer)
self.assertListEqual([n.PrivateKey, n.PublicKey], [privkey, pubkey])
# change ?
peer['Identifier'] = 'changedagain'
n = self.c.PutPeer(PublicKey=p.PublicKey, Peer=peer)
self.assertListEqual([n.PrivateKey, n.PublicKey], [privkey, pubkey])
# invalid change operations
n = peer.copy()
n['PrivateKey'], n['PublicKey'] = generate_wireguard_keys()
with self.assertRaisesRegex(ApiError, "PublicKey parameter must match the model public key"):
self.c.PutPeer(PublicKey=p.PublicKey, Peer=n)
with self.assertRaisesRegex(ApiError, "PublicKey parameter must match the model public key"):
self.c.PatchPeer(PublicKey=p.PublicKey, Peer=n)
n = self.c.DeletePeer(PublicKey=p.PublicKey)
def test_Deployment(self):
log.setLevel(logging.DEBUG)
self._client("general")
self.easy_peer()
self.c.GetPeerDeploymentConfig(PublicKey=self.keys.public)
self.c.GetPeerDeploymentInformation(Email=self.user)
log.setLevel(logging.INFO)
def test_User(self):
u = self.c.PostUser(User={"Firstname": "Test", "Lastname": "User", "Email": self.randmail})
for i in self.c.GetUsers():
if i.Email == u.Email:
break
else:
self.assertTrue(False)
u = self.c.GetUser(Email=u.Email)
self.c.PutUser(Email=u.Email, User={"Firstname": "Test", "Lastname": "User", "Email": u.Email})
self.c.PatchUser(Email=u.Email, User={"Firstname": "Test", "Lastname": "User", "Email": u.Email})
# list a deleted user
self.c.DeleteUser(Email=u.Email)
for i in self.c.GetUsers():
break
def _clear_peers(self):
for p in self.c.GetPeers(DeviceName=self.DEVICE):
self.c.DeletePeer(PublicKey=p.PublicKey)
def _clear_users(self):
for p in self.c.GetUsers():
if p.Email == self.AUTH['api'][1][0]:
continue
self.c.DeleteUser(Email=p.Email)
def _createPeer(self):
privkey, pubkey = generate_wireguard_keys()
peer = {"UID": uuid.uuid4().hex,
"Identifier": uuid.uuid4().hex,
"DeviceName": self.DEVICE,
"PublicKey": pubkey,
"PrivateKey": privkey,
"DeviceType": "client",
# "IPsStr": str(self.ifaddr),
"Email": self.user}
self.c.PostPeer(DeviceName=self.DEVICE, Peer=peer)
return pubkey
def test_address_exhaustion(self):
global log
self._clear_peers()
self._clear_users()
self.NETWORK = ipaddress.ip_network("10.0.0.0/29")
addr = ipaddress.ip_address(
random.randrange(int(self.NETWORK.network_address) + 1, int(self.NETWORK.broadcast_address) - 1))
self.__class__.IFADDR = str(ipaddress.ip_interface(f"{addr}/{self.NETWORK.prefixlen}"))
# reconfigure via web ui - set the ifaddr with less addrs in pool
self.finishInstallation()
keys = set()
EADDRESSEXHAUSTED = "failed to get available IP addresses: no more available address from cidr"
with self.assertRaisesRegex(ValueError, EADDRESSEXHAUSTED):
for i in range(self.NETWORK.num_addresses + 1):
keys.add(self._createPeer())
n = keys.pop()
self.c.DeletePeer(PublicKey=n)
self._createPeer()
with self.assertRaisesRegex(ValueError, EADDRESSEXHAUSTED):
self._createPeer()
# expand network
self.NETWORK = ipaddress.ip_network("10.0.0.0/28")
addr = ipaddress.ip_address(
random.randrange(int(self.NETWORK.network_address) + 1, int(self.NETWORK.broadcast_address) - 1))
self.__class__.IFADDR = str(ipaddress.ip_interface(f"{addr}/{self.NETWORK.prefixlen}"))
self.finishInstallation()
self._createPeer()