Compare commits

...

62 Commits

Author SHA1 Message Date
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
Christoph Haas
e0968b3239 support AllowIPs for peers in server config (#24) 2021-06-18 14:13:44 +02:00
Christoph Haas
e1db939a18 update readme to clarify some things 2021-06-18 14:12:22 +02:00
Christoph Haas
92d09535bc fix foreign key problem (#23) 2021-06-08 16:17:30 +02:00
Christoph Haas
d165fc0658 ensure that email attribute is set 2021-05-17 09:06:26 +02:00
Christoph Haas
cadbe4a090 fix migrations for fresh databases 2021-05-16 23:55:59 +02:00
Christoph Haas
d516d74d3f ldap filter: skip empty emails by default, fix ldap sync (case insensitive email comparison, user source difference) 2021-05-16 23:43:47 +02:00
Christoph Haas
c9e7145a5b add docker latest tag warning to README.md 2021-05-16 23:14:18 +02:00
h44z
88278bf677 Merge pull request #21 from h44z/ldap_filter
use LDAP filter strings
2021-05-16 23:11:55 +02:00
Christoph Haas
1c4d47293c skip migrations for fresh databases 2021-05-16 23:11:03 +02:00
Christoph Haas
27de6e8b8c use LDAP filter strings 2021-05-10 10:31:56 +02:00
Christoph Haas
3ecb0925d6 use low error correction if qr-code content is too long (#18) 2021-05-10 09:26:36 +02:00
Christoph Haas
edfecd536a use query params throughout the whole rest api (#11) 2021-05-03 11:40:06 +02:00
Christoph Haas
d794f807ad use query param for public key in api requests (#11) 2021-05-03 10:44:27 +02:00
h44z
84e5359977 Merge pull request #16 from xhit/patch-1
Fix email encryption type SSL/TLS
2021-04-30 11:00:25 +02:00
Santiago De la Cruz
5ac45b7a4f Fix email encryption type SSL/TLS
mail.EncryptionTLS is deprecated and is the same like mail.EncryptionSTARTTLS

The correct here is mail.EncryptionSSLTLS
2021-04-29 12:53:03 -04:00
Christoph Haas
ab02f656be add ServerName to TLS config 2021-04-29 18:19:41 +02:00
Christoph Haas
0d4e12a6c1 increase smtp timeout to 30 seconds 2021-04-29 17:04:26 +02:00
Christoph Haas
9a420d26e1 use html email body by default, add alternative text only body 2021-04-29 16:54:01 +02:00
Christoph Haas
19e6fa2a1a switch to another email lib to support more AUTH types 2021-04-29 16:45:28 +02:00
Christoph Haas
7b1f59d86a deployment api completed (#11) 2021-04-29 11:23:32 +02:00
Christoph Haas
9c8a1df01f Set server name in TLS config (#13) 2021-04-29 10:59:00 +02:00
Christoph Haas
87964f8ec4 RESTful API for WireGuard Portal (#11) 2021-04-26 22:00:50 +02:00
Christoph Haas
35513ae994 WIP: RESTful API for WireGuard Portal, user endpoint (#11) 2021-04-26 20:02:40 +02:00
Christoph Haas
b6d9814021 use lowercase email addresses for filtering (#14) 2021-04-22 20:46:03 +02:00
Christoph Haas
97edd103be transform email addresses to lower case in ldap sync (#14) 2021-04-22 20:41:30 +02:00
Christoph Haas
e052f400aa convert all email addresses to lower case (#14) 2021-04-22 20:29:37 +02:00
Christoph Haas
926733dea4 add ssl/tls option for email encryption (#13) 2021-04-22 14:11:54 +02:00
Christoph Haas
7042523c54 configurable cert-check for the ldap auth provider (#12) 2021-04-21 11:07:16 +02:00
Christoph Haas
e65a4a8148 disable cert-check should also work for ldap via ssl (#12) 2021-04-21 10:04:10 +02:00
Christoph Haas
28c2494d88 cleanup import statements 2021-04-09 23:17:44 +02:00
Christoph Haas
11b9a567d1 include tag version in travis builds 2021-04-08 21:30:16 +02:00
Christoph Haas
f34594f8d2 fix allowed ip's for peers in server-mode 2021-04-08 19:10:38 +02:00
Christoph Haas
46dc6dc2ad remove endpoint from peer in server-mode 2021-04-08 18:39:52 +02:00
Christoph Haas
2ca1226d50 fix .local DNS lookup (https://github.com/golang/go/issues/35067) 2021-04-08 18:37:49 +02:00
Christoph Haas
066f939294 fix version display 2021-04-08 18:10:53 +02:00
Christoph Haas
17bc297d77 WIP: smaller docker image, fix docker build 2021-04-08 17:58:25 +02:00
Christoph Haas
79e4513edb WIP: smaller docker image, sqlite needs cgo 2021-04-08 09:38:32 +02:00
Christoph Haas
f793ece922 WIP: smaller docker image 2021-04-08 09:23:48 +02:00
Christoph Haas
96215c4f0e version 1.0.6, show version in footer 2021-04-07 20:07:40 +02:00
Christoph Haas
5199c8674d add configuration options and sample yaml to readme (#6) 2021-04-06 23:51:57 +02:00
Christoph Haas
2caa64571b fix duplicate configuration tag (#6) 2021-04-06 23:01:50 +02:00
Christoph Haas
afbe36d289 fix client edit layout 2021-04-06 00:33:51 +02:00
56 changed files with 3999 additions and 605 deletions

55
.circleci/config.yml Normal file
View File

@@ -0,0 +1,55 @@
version: 2.1
jobs:
build:
working_directory: ~/repo
docker:
- image: circleci/golang:1.16.7
steps:
- checkout
- restore_cache:
keys:
- go-mod-v4-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: |
make dep
- save_cache:
key: go-mod-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: ~/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
workflows:
build-and-release:
jobs:
#--------------- BUILD ---------------#
- build:
name: build
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,4 +31,6 @@ data/
ssh.key ssh.key
.testCoverage.txt .testCoverage.txt
wg_portal.db wg_portal.db
go.sum swagger.json
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 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 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

@@ -6,6 +6,12 @@
######- ######-
FROM golang:1.16 as builder FROM golang:1.16 as builder
ARG BUILD_IDENTIFIER
ENV ENV_BUILD_IDENTIFIER=$BUILD_IDENTIFIER
ARG BUILD_VERSION
ENV ENV_BUILD_VERSION=$BUILD_VERSION
RUN mkdir /build RUN mkdir /build
# Copy the source from the current directory to the Working Directory inside the container # Copy the source from the current directory to the Working Directory inside the container
@@ -17,28 +23,32 @@ WORKDIR /build
# Workaround for failing travis-ci builds # Workaround for failing travis-ci builds
RUN rm -rf ~/go; rm -rf go.sum RUN rm -rf ~/go; rm -rf go.sum
# Download dependencies
RUN curl -L https://git.prolicht.digital/pub/healthcheck/-/releases/v1.0.1/downloads/binaries/hc -o /build/hc; \
chmod +rx /build/hc; \
echo "Building version: $ENV_BUILD_IDENTIFIER-$ENV_BUILD_VERSION"
# Build the Go app # Build the Go app
RUN go clean -modcache; go mod tidy; make build RUN go clean -modcache; go mod tidy; make build-docker
######- ######-
# Here starts the main image # Here starts the main image
######- ######-
FROM debian:buster FROM scratch
# Setup timezone # Setup timezone
ENV TZ=Europe/Vienna ENV TZ=Europe/Vienna
# GOSS for container health checks # Import linux stuff from builder.
ENV GOSS_VERSION v0.3.16 COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
RUN apt-get update && apt-get upgrade -y && \ COPY --from=builder /etc/passwd /etc/passwd
apt-get install --no-install-recommends -y moreutils ca-certificates curl && \ COPY --from=builder /etc/group /etc/group
rm -rf /var/cache/apt /var/lib/apt/lists/*; \
curl -L https://github.com/aelsabbahy/goss/releases/download/$GOSS_VERSION/goss-linux-amd64 -o /usr/local/bin/goss && \
chmod +rx /usr/local/bin/goss && \
goss --version
COPY --from=builder /build/dist/wg-portal-amd64 /app/wgportal # Import healthcheck binary
COPY --from=builder /build/scripts /app/ COPY --from=builder /build/hc /app/hc
# Copy binaries
COPY --from=builder /build/dist/wgportal /app/wgportal
# Set the Current Working Directory inside the container # Set the Current Working Directory inside the container
WORKDIR /app WORKDIR /app
@@ -46,5 +56,4 @@ WORKDIR /app
# Command to run the executable # Command to run the executable
CMD [ "/app/wgportal" ] CMD [ "/app/wgportal" ]
HEALTHCHECK --interval=1m --timeout=10s \ HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD [ "/app/hc", "http://localhost:11223/health" ]
CMD /app/docker-healthcheck.sh

View File

@@ -18,6 +18,9 @@ build-cross-plat: dep build $(addsuffix -arm,$(addprefix $(BUILDDIR)/,$(BINARIES
cp scripts/wg-portal.service $(BUILDDIR) cp scripts/wg-portal.service $(BUILDDIR)
cp scripts/wg-portal.env $(BUILDDIR) cp scripts/wg-portal.env $(BUILDDIR)
build-docker: dep
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(GOCMD) build -o $(BUILDDIR)/wgportal -ldflags "-w -s -linkmode external -extldflags \"-static\" -X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -tags netgo cmd/wg-portal/main.go
dep: dep:
$(GOCMD) mod download $(GOCMD) mod download
@@ -48,13 +51,17 @@ docker-build:
docker-push: docker-push:
docker push $(IMAGE) docker push $(IMAGE)
api-docs:
cd internal/server; swag init --parseDependency --parseInternal --generalInfo api.go
$(GOCMD) fmt internal/server/docs/docs.go
$(BUILDDIR)/%-amd64: cmd/%/main.go dep phony $(BUILDDIR)/%-amd64: cmd/%/main.go dep phony
GOOS=linux GOARCH=amd64 $(GOCMD) build -o $@ $< GOOS=linux GOARCH=amd64 $(GOCMD) build -ldflags "-X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $<
# On arch-linux install aarch64-linux-gnu-gcc to crosscompile for arm64 # On arch-linux install aarch64-linux-gnu-gcc to crosscompile for arm64
$(BUILDDIR)/%-arm64: cmd/%/main.go dep phony $(BUILDDIR)/%-arm64: cmd/%/main.go dep phony
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(GOCMD) build -ldflags "-linkmode external -extldflags -static" -o $@ $< CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(GOCMD) build -ldflags "-linkmode external -extldflags \"-static\" -X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $<
# On arch-linux install arm-linux-gnueabihf-gcc to crosscompile for arm # On arch-linux install arm-linux-gnueabihf-gcc to crosscompile for arm
$(BUILDDIR)/%-arm: cmd/%/main.go dep phony $(BUILDDIR)/%-arm: cmd/%/main.go dep phony
CGO_ENABLED=1 CC=arm-linux-gnueabi-gcc GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -ldflags "-linkmode external -extldflags -static" -o $@ $< CGO_ENABLED=1 CC=arm-linux-gnueabi-gcc GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -ldflags "-linkmode external -extldflags \"-static\" -X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $<

123
README.md
View File

@@ -9,11 +9,11 @@
[![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 the VPN The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
interface. 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, 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.
It also supports LDAP (Active Directory or OpenLDAP) as authentication provider. It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
## Features ## Features
@@ -30,14 +30,19 @@ It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
* One single binary * One single binary
* 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
![Screenshot](screenshot.png) ![Screenshot](screenshot.png)
## Setup ## Setup
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.
### Docker ### Docker
The easiest way to run WireGuard Portal is to use the Docker image provided. The easiest way to run WireGuard Portal is to use the Docker image provided.
HINT: the *latest* tag always refers to the master branch and might contain unstable or incompatible code!
Docker Compose snippet with some sample configuration values: Docker Compose snippet with some sample configuration values:
``` ```
version: '3.6' version: '3.6'
@@ -82,7 +87,7 @@ If needed, please make sure to backup 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
@@ -94,11 +99,113 @@ make build-cross-plat
The compiled binary will be located in the dist folder. The compiled binary will be located in the dist folder.
A detailed description for using this software with a raspberry pi can be found in the [README-RASPBERRYPI.md](README-RASPBERRYPI.md). A detailed description for using this software with a raspberry pi can be found in the [README-RASPBERRYPI.md](README-RASPBERRYPI.md).
## What is out of scope ## Configuration
You can configure WireGuard Portal using either environment variables or a yaml configuration file.
The filepath of the yaml configuration file defaults to **config.yml** in the working directory of the executable.
It is possible to override the configuration filepath using the environment variable **CONFIG_FILE**.
For example: `CONFIG_FILE=/home/test/config.yml ./wg-portal-amd64`.
* Generation or application of any `iptables` or `nftables` rules ### Configuration Options
* Setting up or changing IP-addresses of the WireGuard interface on operating systems other than linux The following configuration options are available:
* Importing private keys of an existing WireGuard setup
| 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. |
| 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. |
| 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. |
| ADMIN_USER | adminUser | core | admin@wgportal.local | The administrator user. Must be a valid email address. |
| ADMIN_PASS | adminPass | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
| EDITABLE_KEYS | editableKeys | core | true | Allow to edit key-pairs in the UI. |
| 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. |
| SELF_PROVISIONING | selfProvisioning | core | false | Allow registered users to automatically create peers via the RESTful API. |
| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. |
| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. |
| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. |
| DATABASE_HOST | host | database | | The mysql server address. |
| DATABASE_PORT | port | database | | The mysql server port. |
| DATABASE_NAME | database | database | data/wg_portal.db | For sqlite database: the database file-path, otherwise the database name. |
| DATABASE_USERNAME | user | database | | The mysql user. |
| DATABASE_PASSWORD | password | database | | The mysql password. |
| EMAIL_HOST | host | email | 127.0.0.1 | The email server address. |
| EMAIL_PORT | port | email | 25 | The email server port. |
| EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. |
| EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. |
| EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. |
| EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. |
| EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. |
| EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. |
| WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. |
| WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). |
| WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. |
| MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. |
| LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. |
| LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. |
| LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. |
| LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. |
| LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. |
| LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. |
| 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_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_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. |
| LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. |
| LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. |
| LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. |
| LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. |
| LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. |
| 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
config.yml:
```yaml
core:
listeningAddress: :8123
externalUrl: https://wg-test.test.com
adminUser: test@test.com
adminPass: test
editableKeys: true
createDefaultPeer: false
ldapEnabled: true
mailFrom: WireGuard VPN <noreply@test.com>
ldap:
url: ldap://10.10.10.10:389
dn: DC=test,DC=test
startTLS: false
user: wireguard@test.test
pass: test
adminGroup: CN=WireGuardAdmins,CN=Users,DC=test,DC=test
database:
typ: sqlite
database: data/wg_portal.db
email:
host: smtp.gmail.com
port: 587
tls: true
user: test@gmail.com
pass: topsecret
wg:
devices:
- wg0
- wg1
defaultDevice: wg0
configDirectory: /etc/wireguard
manageIPAddresses: true
```
### RESTful API
WireGuard Portal offers a RESTful API to interact with.
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`.
## What is out of scope
* Creating or removing WireGuard (wgX) interfaces.
* 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.
* Importing private keys of an existing WireGuard setup.
## Application stack ## Application stack

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}

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

@@ -82,6 +82,12 @@
<input type="text" name="allowedip" class="form-control" id="server_AllowedIP" value="{{.Peer.AllowedIPsStr}}"> <input type="text" name="allowedip" class="form-control" id="server_AllowedIP" value="{{.Peer.AllowedIPsStr}}">
</div> </div>
</div> </div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_AllowedIPSrv">Extra Allowed IPs (Server sided)</label>
<input type="text" name="allowedipSrv" class="form-control" id="server_AllowedIPSrv" value="{{.Peer.AllowedIPsSrvStr}}">
</div>
</div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12 global-config"> <div class="form-group col-md-12 global-config">
<label for="server_DNS">Client DNS Servers</label> <label for="server_DNS">Client DNS Servers</label>
@@ -172,13 +178,11 @@
<label for="client_PersistentKeepalive">Persistent Keepalive (0 = off)</label> <label for="client_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
<input type="number" name="keepalive" class="form-control" id="client_PersistentKeepalive" placeholder="16" value="{{.Peer.PersistentKeepalive}}"> <input type="number" name="keepalive" class="form-control" id="client_PersistentKeepalive" placeholder="16" value="{{.Peer.PersistentKeepalive}}">
</div> </div>
<div class="form-row">
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label for="client_IP">Ping-Check IP Address</label> <label for="client_IP">Ping-Check IP Address</label>
<input type="text" name="ip" class="form-control" id="client_IP" value="{{.Peer.IPsStr}}"> <input type="text" name="ip" class="form-control" id="client_IP" value="{{.Peer.IPsStr}}">
</div> </div>
</div> </div>
</div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12"> <div class="form-group col-md-12">
@@ -199,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

@@ -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

@@ -80,8 +80,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

@@ -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

@@ -23,8 +23,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

@@ -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

@@ -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

@@ -1,5 +1,5 @@
<footer class="page-footer mt-auto"> <footer class="page-footer mt-auto">
<div class="container mt-3"> <div class="container mt-3">
<p class="text-muted">Copyright © {{ $.Static.CompanyName }} {{$.Static.Year}} <a class="scroll-to-top" href="#page-top"><i class="fas fa-angle-up"></i></a></p> <p class="text-muted">Copyright © {{ $.Static.CompanyName }} {{$.Static.Year}}, version {{$.Static.Version}} <a class="float-right scroll-to-top" href="#page-top"><i class="fas fa-angle-up"></i></a></p>
</div> </div>
</footer> </footer>

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

@@ -8,24 +8,26 @@ import (
"syscall" "syscall"
"time" "time"
"git.prolicht.digital/pub/healthcheck"
"github.com/h44z/wg-portal/internal/server" "github.com/h44z/wg-portal/internal/server"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var Version = "unknown (local build)"
func main() { func main() {
_ = setupLogger(logrus.StandardLogger()) _ = setupLogger(logrus.StandardLogger())
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
logrus.Infof("starting WireGuard Portal Server [%s]...", Version) logrus.Infof("starting WireGuard Portal Server [%s]...", server.Version)
// Context for clean shutdown // Context for clean shutdown
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
// start health check service on port 11223
healthcheck.New(healthcheck.WithContext(ctx)).Start()
service := server.Server{} service := server.Server{}
if err := service.Setup(ctx); err != nil { if err := service.Setup(ctx); err != nil {
logrus.Fatalf("setup failed: %v", err) logrus.Fatalf("setup failed: %v", err)

View File

@@ -1,7 +1,7 @@
version: '3.6' version: '3.6'
services: services:
wg-portal: wg-portal:
image: h44z/wg-portal:latest image: h44z/wg-portal:1.0.6
container_name: wg-portal container_name: wg-portal
restart: unless-stopped restart: unless-stopped
cap_add: cap_add:

25
go.mod
View File

@@ -3,24 +3,33 @@ module github.com/h44z/wg-portal
go 1.16 go 1.16
require ( require (
git.prolicht.digital/pub/healthcheck v1.0.1
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
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-playground/validator/v10 v10.4.1 github.com/go-openapi/swag v0.19.15 // indirect
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/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible
github.com/kelseyhightower/envconfig v1.4.0 github.com/kelseyhightower/envconfig v1.4.0
github.com/mailru/easyjson v0.7.7 // indirect
github.com/milosgajdos/tenus v0.0.3 github.com/milosgajdos/tenus v0.0.3
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.1
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
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 github.com/xhit/go-simple-mail/v2 v2.10.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/tools v0.1.5 // indirect
golang.zx2c4.com/wireguard v0.0.20200121 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
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=

10
hooks/build Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
# File needs to be called /hooks/build relative to the Dockerfile.
# Some environment variables are injected into the build hook, see: https://docs.docker.com/docker-hub/builds/advanced/.
GIT_SHORT_HASH=$(echo $SOURCE_COMMIT | cut -c1-7)
echo "Build hook running for git hash $GIT_SHORT_HASH"
docker build --build-arg BUILD_IDENTIFIER=$DOCKER_TAG \
--build-arg BUILD_VERSION=$GIT_SHORT_HASH \
-t $IMAGE_NAME .

View File

@@ -2,7 +2,6 @@ package ldap
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -69,13 +68,11 @@ func (provider Provider) Login(ctx *authentication.AuthContext) (string, error)
// Search for the given username // Search for the given username
attrs := []string{"dn", provider.config.EmailAttribute} attrs := []string{"dn", provider.config.EmailAttribute}
if provider.config.DisabledAttribute != "" { loginFilter := strings.Replace(provider.config.LoginFilter, "{{login_identifier}}", username, -1)
attrs = append(attrs, provider.config.DisabledAttribute)
}
searchRequest := ldap.NewSearchRequest( searchRequest := ldap.NewSearchRequest(
provider.config.BaseDN, provider.config.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass=%s)(%s=%s))", provider.config.UserClass, provider.config.EmailAttribute, username), loginFilter,
attrs, attrs,
nil, nil,
) )
@@ -89,24 +86,8 @@ func (provider Provider) Login(ctx *authentication.AuthContext) (string, error)
return "", errors.Errorf("invalid amount of ldap entries (%d)", len(sr.Entries)) return "", errors.Errorf("invalid amount of ldap entries (%d)", len(sr.Entries))
} }
userDN := sr.Entries[0].DN
// Check if user is disabled, if so deny login
if provider.config.DisabledAttribute != "" {
uac := sr.Entries[0].GetAttributeValue(provider.config.DisabledAttribute)
switch provider.config.Type {
case ldapconfig.TypeActiveDirectory:
if ldapconfig.IsActiveDirectoryUserDisabled(uac) {
return "", errors.New("user is disabled")
}
case ldapconfig.TypeOpenLDAP:
if ldapconfig.IsOpenLdapUserDisabled(uac) {
return "", errors.New("user is disabled")
}
}
}
// Bind as the user to verify their password // Bind as the user to verify their password
userDN := sr.Entries[0].DN
err = client.Bind(userDN, password) err = client.Bind(userDN, password)
if err != nil { if err != nil {
return "", errors.Wrapf(err, "invalid credentials") return "", errors.Wrapf(err, "invalid credentials")
@@ -136,13 +117,11 @@ func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authent
// Search for the given username // Search for the given username
attrs := []string{"dn", provider.config.EmailAttribute, provider.config.FirstNameAttribute, provider.config.LastNameAttribute, attrs := []string{"dn", provider.config.EmailAttribute, provider.config.FirstNameAttribute, provider.config.LastNameAttribute,
provider.config.PhoneAttribute, provider.config.GroupMemberAttribute} provider.config.PhoneAttribute, provider.config.GroupMemberAttribute}
if provider.config.DisabledAttribute != "" { loginFilter := strings.Replace(provider.config.LoginFilter, "{{login_identifier}}", username, -1)
attrs = append(attrs, provider.config.DisabledAttribute)
}
searchRequest := ldap.NewSearchRequest( searchRequest := ldap.NewSearchRequest(
provider.config.BaseDN, provider.config.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass=%s)(%s=%s))", provider.config.UserClass, provider.config.EmailAttribute, username), loginFilter,
attrs, attrs,
nil, nil,
) )
@@ -175,14 +154,15 @@ func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authent
} }
func (provider Provider) open() (*ldap.Conn, error) { func (provider Provider) open() (*ldap.Conn, error) {
conn, err := ldap.DialURL(provider.config.URL) tlsConfig := &tls.Config{InsecureSkipVerify: !provider.config.CertValidation}
conn, err := ldap.DialURL(provider.config.URL, ldap.DialWithTLSConfig(tlsConfig))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if provider.config.StartTLS { if provider.config.StartTLS {
// Reconnect with TLS // Reconnect with TLS
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !provider.config.CertValidation}) err = conn.StartTLS(tlsConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -7,10 +7,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/h44z/wg-portal/internal/common"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/authentication" "github.com/h44z/wg-portal/internal/authentication"
"github.com/h44z/wg-portal/internal/common"
"github.com/h44z/wg-portal/internal/users" "github.com/h44z/wg-portal/internal/users"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@@ -109,6 +108,7 @@ func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authent
} }
func (provider Provider) InitializeAdmin(email, password string) error { func (provider Provider) InitializeAdmin(email, password string) error {
email = strings.ToLower(email)
if !emailRegex.MatchString(email) { if !emailRegex.MatchString(email) {
return errors.New("admin username must be an email address") return errors.New("admin username must be an email address")
} }
@@ -136,7 +136,7 @@ func (provider Provider) InitializeAdmin(email, password string) error {
} }
admin.Email = email admin.Email = email
admin.Password = string(hashedPassword) admin.Password = users.PrivateString(hashedPassword)
admin.Firstname = "WireGuard" admin.Firstname = "WireGuard"
admin.Lastname = "Administrator" admin.Lastname = "Administrator"
admin.CreatedAt = time.Now() admin.CreatedAt = time.Now()
@@ -170,7 +170,7 @@ func (provider Provider) InitializeAdmin(email, password string) error {
return errors.Wrap(err, "failed to hash admin password") return errors.Wrap(err, "failed to hash admin password")
} }
admin.Password = string(hashedPassword) admin.Password = users.PrivateString(hashedPassword)
admin.IsAdmin = true admin.IsAdmin = true
admin.UpdatedAt = time.Now() admin.UpdatedAt = time.Now()

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -14,6 +15,29 @@ import (
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
) )
func init() {
migrations = append(migrations, Migration{
version: "1.0.7",
migrateFn: func(db *gorm.DB) error {
if err := db.Exec("UPDATE users SET email = LOWER(email)").Error; err != nil {
return errors.Wrap(err, "failed to convert user emails to lower case")
}
if err := db.Exec("UPDATE peers SET email = LOWER(email)").Error; err != nil {
return errors.Wrap(err, "failed to convert peer emails to lower case")
}
logrus.Infof("upgraded database format to version 1.0.7")
return nil
},
})
migrations = append(migrations, Migration{
version: "1.0.8",
migrateFn: func(db *gorm.DB) error {
logrus.Infof("upgraded database format to version 1.0.8")
return nil
},
})
}
type SupportedDatabase string type SupportedDatabase string
const ( const (
@@ -80,16 +104,18 @@ type DatabaseMigrationInfo struct {
Applied time.Time Applied time.Time
} }
type Migration struct {
version string
migrateFn func(db *gorm.DB) error
}
var migrations []Migration
func MigrateDatabase(db *gorm.DB, version string) error { func MigrateDatabase(db *gorm.DB, version string) error {
if err := db.AutoMigrate(&DatabaseMigrationInfo{}); err != nil { if err := db.AutoMigrate(&DatabaseMigrationInfo{}); err != nil {
return errors.Wrap(err, "failed to migrate version database") return errors.Wrap(err, "failed to migrate version database")
} }
newVersion := DatabaseMigrationInfo{
Version: version,
Applied: time.Now(),
}
existingMigration := DatabaseMigrationInfo{} existingMigration := DatabaseMigrationInfo{}
db.Where("version = ?", version).FirstOrInit(&existingMigration) db.Where("version = ?", version).FirstOrInit(&existingMigration)
@@ -97,11 +123,36 @@ func MigrateDatabase(db *gorm.DB, version string) error {
lastVersion := DatabaseMigrationInfo{} lastVersion := DatabaseMigrationInfo{}
db.Order("applied desc, version desc").FirstOrInit(&lastVersion) db.Order("applied desc, version desc").FirstOrInit(&lastVersion)
// TODO: migrate database if lastVersion.Version == "" {
// fresh database, no migrations to apply
res := db.Create(&newVersion) res := db.Create(&DatabaseMigrationInfo{
Version: version,
Applied: time.Now(),
})
if res.Error != nil { if res.Error != nil {
return errors.Wrap(res.Error, "failed to write version to database") return errors.Wrapf(res.Error, "failed to write version %s to database", version)
}
return nil
}
sort.Slice(migrations, func(i, j int) bool {
return migrations[i].version < migrations[j].version
})
for _, migration := range migrations {
if migration.version > lastVersion.Version {
if err := migration.migrateFn(db); err != nil {
return errors.Wrapf(err, "failed to migrate to version %s", migration.version)
}
res := db.Create(&DatabaseMigrationInfo{
Version: migration.version,
Applied: time.Now(),
})
if res.Error != nil {
return errors.Wrapf(res.Error, "failed to write version %s to database", migration.version)
}
}
} }
} }

View File

@@ -3,20 +3,38 @@ package common
import ( import (
"crypto/tls" "crypto/tls"
"io" "io"
"net/smtp" "io/ioutil"
"strconv" "time"
"strings"
"github.com/jordan-wright/email" "github.com/pkg/errors"
mail "github.com/xhit/go-simple-mail/v2"
)
type MailEncryption string
const (
MailEncryptionNone MailEncryption = "none"
MailEncryptionTLS MailEncryption = "tls"
MailEncryptionStartTLS MailEncryption = "starttls"
)
type MailAuthType string
const (
MailAuthPlain MailAuthType = "plain"
MailAuthLogin MailAuthType = "login"
MailAuthCramMD5 MailAuthType = "crammd5"
) )
type MailConfig struct { type MailConfig struct {
Host string `yaml:"host" envconfig:"EMAIL_HOST"` Host string `yaml:"host" envconfig:"EMAIL_HOST"`
Port int `yaml:"port" envconfig:"EMAIL_PORT"` Port int `yaml:"port" envconfig:"EMAIL_PORT"`
TLS bool `yaml:"tls" envconfig:"EMAIL_TLS"` TLS bool `yaml:"tls" envconfig:"EMAIL_TLS"` // Deprecated, use MailConfig.Encryption instead.
Encryption MailEncryption `yaml:"encryption" envconfig:"EMAIL_ENCRYPTION"`
CertValidation bool `yaml:"certcheck" envconfig:"EMAIL_CERT_VALIDATION"` CertValidation bool `yaml:"certcheck" envconfig:"EMAIL_CERT_VALIDATION"`
Username string `yaml:"user" envconfig:"EMAIL_USERNAME"` Username string `yaml:"user" envconfig:"EMAIL_USERNAME"`
Password string `yaml:"pass" envconfig:"EMAIL_PASSWORD"` Password string `yaml:"pass" envconfig:"EMAIL_PASSWORD"`
AuthType MailAuthType `yaml:"auth" envconfig:"EMAIL_AUTHTYPE"`
} }
type MailAttachment struct { type MailAttachment struct {
@@ -27,53 +45,73 @@ type MailAttachment struct {
} }
// SendEmailWithAttachments sends a mail with optional attachments. // SendEmailWithAttachments sends a mail with optional attachments.
func SendEmailWithAttachments(cfg MailConfig, sender, replyTo, subject, body string, htmlBody string, receivers []string, attachments []MailAttachment) error { func SendEmailWithAttachments(cfg MailConfig, sender, replyTo, subject, body, htmlBody string, receivers []string, attachments []MailAttachment) error {
e := email.NewEmail() srv := mail.NewSMTPClient()
srv.ConnectTimeout = 30 * time.Second
srv.SendTimeout = 30 * time.Second
srv.Host = cfg.Host
srv.Port = cfg.Port
srv.Username = cfg.Username
srv.Password = cfg.Password
// TODO: remove this once the deprecated MailConfig.TLS config option has been removed
if cfg.TLS {
cfg.Encryption = MailEncryptionStartTLS
}
switch cfg.Encryption {
case MailEncryptionTLS:
srv.Encryption = mail.EncryptionSSLTLS
case MailEncryptionStartTLS:
srv.Encryption = mail.EncryptionSTARTTLS
default: // MailEncryptionNone
srv.Encryption = mail.EncryptionNone
}
srv.TLSConfig = &tls.Config{ServerName: srv.Host, InsecureSkipVerify: !cfg.CertValidation}
switch cfg.AuthType {
case MailAuthPlain:
srv.Authentication = mail.AuthPlain
case MailAuthLogin:
srv.Authentication = mail.AuthLogin
case MailAuthCramMD5:
srv.Authentication = mail.AuthCRAMMD5
}
client, err := srv.Connect()
if err != nil {
return errors.Wrap(err, "failed to connect via SMTP")
}
hostname := cfg.Host + ":" + strconv.Itoa(cfg.Port)
subject = strings.Trim(subject, "\n\r\t")
sender = strings.Trim(sender, "\n\r\t")
replyTo = strings.Trim(replyTo, "\n\r\t")
if replyTo == "" { if replyTo == "" {
replyTo = sender replyTo = sender
} }
var auth smtp.Auth email := mail.NewMSG()
if cfg.Username == "" { email.SetFrom(sender).
auth = nil AddTo(receivers...).
} else { SetReplyTo(replyTo).
// Set up authentication information. SetSubject(subject)
auth = smtp.PlainAuth(
"",
cfg.Username,
cfg.Password,
cfg.Host,
)
}
// Set email data. email.SetBody(mail.TextHTML, htmlBody)
e.From = sender email.AddAlternative(mail.TextPlain, body)
e.To = receivers
e.ReplyTo = []string{replyTo}
e.Subject = subject
e.Text = []byte(body)
if htmlBody != "" {
e.HTML = []byte(htmlBody)
}
for _, attachment := range attachments { for _, attachment := range attachments {
a, err := e.Attach(attachment.Data, attachment.Name, attachment.ContentType) attachmentData, err := ioutil.ReadAll(attachment.Data)
if err != nil { if err != nil {
return err return errors.Wrapf(err, "failed to read attachment data for %s", attachment.Name)
} }
if attachment.Embedded { if attachment.Embedded {
a.HTMLRelated = true email.AddInlineData(attachmentData, attachment.Name, attachment.ContentType)
} else {
email.AddAttachmentData(attachmentData, attachment.Name, attachment.ContentType)
} }
} }
if cfg.TLS { // Call Send and pass the client
return e.SendWithStartTLS(hostname, auth, &tls.Config{InsecureSkipVerify: !cfg.CertValidation}) err = email.Send(client)
} else { if err != nil {
return e.Send(hostname, auth) return errors.Wrapf(err, "failed to send email")
} }
return nil
} }

View File

@@ -15,14 +15,13 @@ type Config struct {
BindUser string `yaml:"user" envconfig:"LDAP_USER"` BindUser string `yaml:"user" envconfig:"LDAP_USER"`
BindPass string `yaml:"pass" envconfig:"LDAP_PASSWORD"` BindPass string `yaml:"pass" envconfig:"LDAP_PASSWORD"`
Type Type `yaml:"typ" envconfig:"LDAP_TYPE"` // AD for active directory, OpenLDAP for OpenLDAP
UserClass string `yaml:"userClass" envconfig:"LDAP_USER_CLASS"`
EmailAttribute string `yaml:"attrEmail" envconfig:"LDAP_ATTR_EMAIL"` EmailAttribute string `yaml:"attrEmail" envconfig:"LDAP_ATTR_EMAIL"`
FirstNameAttribute string `yaml:"attrFirstname" envconfig:"LDAP_ATTR_FIRSTNAME"` FirstNameAttribute string `yaml:"attrFirstname" envconfig:"LDAP_ATTR_FIRSTNAME"`
LastNameAttribute string `yaml:"attrLastname" envconfig:"LDAP_ATTR_LASTNAME"` LastNameAttribute string `yaml:"attrLastname" envconfig:"LDAP_ATTR_LASTNAME"`
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"`
DisabledAttribute string `yaml:"attrDisabled" envconfig:"LDAP_ATTR_DISABLED"`
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"`
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
} }

View File

@@ -2,8 +2,6 @@ package ldap
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"strconv"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -16,14 +14,15 @@ type RawLdapData struct {
} }
func Open(cfg *Config) (*ldap.Conn, error) { func Open(cfg *Config) (*ldap.Conn, error) {
conn, err := ldap.DialURL(cfg.URL) tlsConfig := &tls.Config{InsecureSkipVerify: !cfg.CertValidation}
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")
} }
if cfg.StartTLS { if cfg.StartTLS {
// Reconnect with TLS // Reconnect with TLS
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !cfg.CertValidation}) err = conn.StartTLS(tlsConfig)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to star TLS on connection") return nil, errors.Wrap(err, "failed to star TLS on connection")
} }
@@ -53,13 +52,10 @@ func FindAllUsers(cfg *Config) ([]RawLdapData, error) {
// Search all users // Search all users
attrs := []string{"dn", cfg.EmailAttribute, cfg.EmailAttribute, cfg.FirstNameAttribute, cfg.LastNameAttribute, attrs := []string{"dn", cfg.EmailAttribute, cfg.EmailAttribute, cfg.FirstNameAttribute, cfg.LastNameAttribute,
cfg.PhoneAttribute, cfg.GroupMemberAttribute} cfg.PhoneAttribute, cfg.GroupMemberAttribute}
if cfg.DisabledAttribute != "" {
attrs = append(attrs, cfg.DisabledAttribute)
}
searchRequest := ldap.NewSearchRequest( searchRequest := ldap.NewSearchRequest(
cfg.BaseDN, cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(objectClass=%s)", cfg.UserClass), attrs, nil, cfg.SyncFilter, attrs, nil,
) )
sr, err := client.Search(searchRequest) sr, err := client.Search(searchRequest)
@@ -86,27 +82,3 @@ func FindAllUsers(cfg *Config) ([]RawLdapData, error) {
return tmpData, nil return tmpData, nil
} }
func IsActiveDirectoryUserDisabled(userAccountControl string) bool {
if userAccountControl == "" {
return false
}
uacInt, err := strconv.ParseInt(userAccountControl, 10, 32)
if err != nil {
return true
}
if int32(uacInt)&0x2 != 0 {
return true // bit 2 set means account is disabled
}
return false
}
func IsOpenLdapUserDisabled(pwdAccountLockedTime string) bool {
if pwdAccountLockedTime != "" {
return true
}
return false
}

926
internal/server/api.go Normal file
View File

@@ -0,0 +1,926 @@
package server
// go get -u github.com/swaggo/swag/cmd/swag
// run: swag init --parseDependency --parseInternal --generalInfo api.go
// in the internal/server folder
import (
"encoding/json"
"net/http"
"strings"
"time"
jsonpatch "github.com/evanphx/json-patch"
"github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/common"
"github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard"
)
// @title WireGuard Portal API
// @version 1.0
// @description WireGuard Portal API for managing users and peers.
// @license.name MIT
// @license.url https://github.com/h44z/wg-portal/blob/master/LICENSE.txt
// @contact.name WireGuard Portal Project
// @contact.url https://github.com/h44z/wg-portal
// @securityDefinitions.basic ApiBasicAuth
// @in header
// @name Authorization
// @scope.admin Admin access required
// @securityDefinitions.basic GeneralBasicAuth
// @in header
// @name Authorization
// @scope.user User access required
// @BasePath /api/v1
// ApiServer is a simple wrapper struct so that we can have fresh member function names.
type ApiServer struct {
s *Server
}
type ApiError struct {
Message string
}
// GetUsers godoc
// @Tags Users
// @Summary Retrieves all users
// @Produce json
// @Success 200 {object} []users.User
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Router /backend/users [get]
// @Security ApiBasicAuth
func (s *ApiServer) GetUsers(c *gin.Context) {
allUsers := s.s.users.GetUsersUnscoped()
c.JSON(http.StatusOK, allUsers)
}
// GetUser godoc
// @Tags Users
// @Summary Retrieves user based on given Email
// @Produce json
// @Param email query string true "User Email"
// @Success 200 {object} users.User
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Router /backend/user [get]
// @Security ApiBasicAuth
func (s *ApiServer) GetUser(c *gin.Context) {
email := strings.ToLower(strings.TrimSpace(c.Query("email")))
if email == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
return
}
user := s.s.users.GetUserUnscoped(email)
if user == nil {
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
return
}
c.JSON(http.StatusOK, user)
}
// PostUser godoc
// @Tags Users
// @Summary Creates a new user based on the given user model
// @Accept json
// @Produce json
// @Param user body users.User true "User Model"
// @Success 200 {object} users.User
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Failure 500 {object} ApiError
// @Router /backend/users [post]
// @Security ApiBasicAuth
func (s *ApiServer) PostUser(c *gin.Context) {
newUser := users.User{}
if err := c.BindJSON(&newUser); err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return
}
if user := s.s.users.GetUserUnscoped(newUser.Email); user != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: "user already exists"})
return
}
if err := s.s.CreateUser(newUser, s.s.wg.Cfg.GetDefaultDeviceName()); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
user := s.s.users.GetUserUnscoped(newUser.Email)
if user == nil {
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
return
}
c.JSON(http.StatusOK, user)
}
// PutUser godoc
// @Tags Users
// @Summary Updates a user based on the given user model
// @Accept json
// @Produce json
// @Param email query string true "User Email"
// @Param user body users.User true "User Model"
// @Success 200 {object} users.User
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Failure 500 {object} ApiError
// @Router /backend/user [put]
// @Security ApiBasicAuth
func (s *ApiServer) PutUser(c *gin.Context) {
email := strings.ToLower(strings.TrimSpace(c.Query("email")))
if email == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
return
}
updateUser := users.User{}
if err := c.BindJSON(&updateUser); err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return
}
// Changing email address is not allowed
if email != updateUser.Email {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must match the model email address"})
return
}
if user := s.s.users.GetUserUnscoped(email); user == nil {
c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"})
return
}
if err := s.s.UpdateUser(updateUser); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
user := s.s.users.GetUserUnscoped(email)
if user == nil {
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
return
}
c.JSON(http.StatusOK, user)
}
// PatchUser godoc
// @Tags Users
// @Summary Updates a user based on the given partial user model
// @Accept json
// @Produce json
// @Param email query string true "User Email"
// @Param user body users.User true "User Model"
// @Success 200 {object} users.User
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Failure 500 {object} ApiError
// @Router /backend/user [patch]
// @Security ApiBasicAuth
func (s *ApiServer) PatchUser(c *gin.Context) {
email := strings.ToLower(strings.TrimSpace(c.Query("email")))
if email == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
return
}
patch, err := c.GetRawData()
if err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return
}
user := s.s.users.GetUserUnscoped(email)
if user == nil {
c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"})
return
}
userData, err := json.Marshal(user)
if err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
mergedUserData, err := jsonpatch.MergePatch(userData, patch)
var mergedUser users.User
err = json.Unmarshal(mergedUserData, &mergedUser)
if err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
// CHanging email address is not allowed
if email != mergedUser.Email {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must match the model email address"})
return
}
if err := s.s.UpdateUser(mergedUser); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
user = s.s.users.GetUserUnscoped(email)
if user == nil {
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
return
}
c.JSON(http.StatusOK, user)
}
// DeleteUser godoc
// @Tags Users
// @Summary Deletes the specified user
// @Produce json
// @Param email query string true "User Email"
// @Success 204 "No content"
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Failure 500 {object} ApiError
// @Router /backend/user [delete]
// @Security ApiBasicAuth
func (s *ApiServer) DeleteUser(c *gin.Context) {
email := strings.ToLower(strings.TrimSpace(c.Query("email")))
if email == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
return
}
var user *users.User
if user = s.s.users.GetUserUnscoped(email); user == nil {
c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"})
return
}
if err := s.s.DeleteUser(*user); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
c.Status(http.StatusNoContent)
}
// GetPeers godoc
// @Tags Peers
// @Summary Retrieves all peers for the given interface
// @Produce json
// @Param device query string true "Device Name"
// @Success 200 {object} []wireguard.Peer
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Router /backend/peers [get]
// @Security ApiBasicAuth
func (s *ApiServer) GetPeers(c *gin.Context) {
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device")))
if deviceName == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
return
}
// validate device name
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
return
}
peers := s.s.peers.GetAllPeers(deviceName)
c.JSON(http.StatusOK, peers)
}
// GetPeer godoc
// @Tags Peers
// @Summary Retrieves the peer for the given public key
// @Produce json
// @Param pkey query string true "Public Key (Base 64)"
// @Success 200 {object} wireguard.Peer
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Router /backend/peer [get]
// @Security ApiBasicAuth
func (s *ApiServer) GetPeer(c *gin.Context) {
pkey := c.Query("pkey")
if pkey == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
return
}
peer := s.s.peers.GetPeerByKey(pkey)
if !peer.IsValid() {
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
return
}
c.JSON(http.StatusOK, peer)
}
// PostPeer godoc
// @Tags Peers
// @Summary Creates a new peer based on the given peer model
// @Accept json
// @Produce json
// @Param device query string true "Device Name"
// @Param peer body wireguard.Peer true "Peer Model"
// @Success 200 {object} wireguard.Peer
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Failure 500 {object} ApiError
// @Router /backend/peers [post]
// @Security ApiBasicAuth
func (s *ApiServer) PostPeer(c *gin.Context) {
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device")))
if deviceName == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
return
}
// validate device name
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
return
}
newPeer := wireguard.Peer{}
if err := c.BindJSON(&newPeer); err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return
}
if peer := s.s.peers.GetPeerByKey(newPeer.PublicKey); peer.IsValid() {
c.JSON(http.StatusBadRequest, ApiError{Message: "peer already exists"})
return
}
if err := s.s.CreatePeer(deviceName, newPeer); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
peer := s.s.peers.GetPeerByKey(newPeer.PublicKey)
if !peer.IsValid() {
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
return
}
c.JSON(http.StatusOK, peer)
}
// PutPeer godoc
// @Tags Peers
// @Summary Updates the given peer based on the given peer model
// @Accept json
// @Produce json
// @Param pkey query string true "Public Key"
// @Param peer body wireguard.Peer true "Peer Model"
// @Success 200 {object} wireguard.Peer
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Failure 500 {object} ApiError
// @Router /backend/peer [put]
// @Security ApiBasicAuth
func (s *ApiServer) PutPeer(c *gin.Context) {
updatePeer := wireguard.Peer{}
if err := c.BindJSON(&updatePeer); err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return
}
pkey := c.Query("pkey")
if pkey == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
return
}
if peer := s.s.peers.GetPeerByKey(pkey); !peer.IsValid() {
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
return
}
// Changing public key is not allowed
if pkey != updatePeer.PublicKey {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must match the model public key"})
return
}
now := time.Now()
if updatePeer.DeactivatedAt != nil {
updatePeer.DeactivatedAt = &now
}
if err := s.s.UpdatePeer(updatePeer, now); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
peer := s.s.peers.GetPeerByKey(updatePeer.PublicKey)
if !peer.IsValid() {
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
return
}
c.JSON(http.StatusOK, peer)
}
// PatchPeer godoc
// @Tags Peers
// @Summary Updates the given peer based on the given partial peer model
// @Accept json
// @Produce json
// @Param pkey query string true "Public Key"
// @Param peer body wireguard.Peer true "Peer Model"
// @Success 200 {object} wireguard.Peer
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Failure 500 {object} ApiError
// @Router /backend/peer [patch]
// @Security ApiBasicAuth
func (s *ApiServer) PatchPeer(c *gin.Context) {
patch, err := c.GetRawData()
if err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return
}
pkey := c.Query("pkey")
if pkey == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
return
}
peer := s.s.peers.GetPeerByKey(pkey)
if !peer.IsValid() {
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
return
}
peerData, err := json.Marshal(peer)
if err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
mergedPeerData, err := jsonpatch.MergePatch(peerData, patch)
var mergedPeer wireguard.Peer
err = json.Unmarshal(mergedPeerData, &mergedPeer)
if err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
if !mergedPeer.IsValid() {
c.JSON(http.StatusBadRequest, ApiError{Message: "invalid peer model"})
return
}
// Changing public key is not allowed
if pkey != mergedPeer.PublicKey {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must match the model public key"})
return
}
now := time.Now()
if mergedPeer.DeactivatedAt != nil {
mergedPeer.DeactivatedAt = &now
}
if err := s.s.UpdatePeer(mergedPeer, now); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
peer = s.s.peers.GetPeerByKey(mergedPeer.PublicKey)
if !peer.IsValid() {
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
return
}
c.JSON(http.StatusOK, peer)
}
// DeletePeer godoc
// @Tags Peers
// @Summary Updates the given peer based on the given partial peer model
// @Produce json
// @Param pkey query string true "Public Key"
// @Success 202 "No Content"
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Failure 500 {object} ApiError
// @Router /backend/peer [delete]
// @Security ApiBasicAuth
func (s *ApiServer) DeletePeer(c *gin.Context) {
pkey := c.Query("pkey")
if pkey == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
return
}
peer := s.s.peers.GetPeerByKey(pkey)
if peer.PublicKey == "" {
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
return
}
if err := s.s.DeletePeer(peer); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
c.Status(http.StatusNoContent)
}
// GetDevices godoc
// @Tags Interface
// @Summary Get all devices
// @Produce json
// @Success 200 {object} []wireguard.Device
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Router /backend/devices [get]
// @Security ApiBasicAuth
func (s *ApiServer) GetDevices(c *gin.Context) {
var devices []wireguard.Device
for _, deviceName := range s.s.config.WG.DeviceNames {
device := s.s.peers.GetDevice(deviceName)
if !device.IsValid() {
continue
}
devices = append(devices, device)
}
c.JSON(http.StatusOK, devices)
}
// GetDevice godoc
// @Tags Interface
// @Summary Get the given device
// @Produce json
// @Param device query string true "Device Name"
// @Success 200 {object} wireguard.Device
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Router /backend/device [get]
// @Security ApiBasicAuth
func (s *ApiServer) GetDevice(c *gin.Context) {
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device")))
if deviceName == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
return
}
// validate device name
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
return
}
device := s.s.peers.GetDevice(deviceName)
if !device.IsValid() {
c.JSON(http.StatusNotFound, ApiError{Message: "device not found"})
return
}
c.JSON(http.StatusOK, device)
}
// PutDevice godoc
// @Tags Interface
// @Summary Updates the given device based on the given device model (UNIMPLEMENTED)
// @Accept json
// @Produce json
// @Param device query string true "Device Name"
// @Param body body wireguard.Device true "Device Model"
// @Success 200 {object} wireguard.Device
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Failure 500 {object} ApiError
// @Router /backend/device [put]
// @Security ApiBasicAuth
func (s *ApiServer) PutDevice(c *gin.Context) {
updateDevice := wireguard.Device{}
if err := c.BindJSON(&updateDevice); err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return
}
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device")))
if deviceName == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
return
}
// validate device name
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
return
}
device := s.s.peers.GetDevice(deviceName)
if !device.IsValid() {
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
return
}
// Changing device name is not allowed
if deviceName != updateDevice.DeviceName {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must match the model device name"})
return
}
// TODO: implement
c.JSON(http.StatusNotImplemented, device)
}
// PatchDevice godoc
// @Tags Interface
// @Summary Updates the given device based on the given partial device model (UNIMPLEMENTED)
// @Accept json
// @Produce json
// @Param device query string true "Device Name"
// @Param body body wireguard.Device true "Device Model"
// @Success 200 {object} wireguard.Device
// @Failure 400 {object} ApiError
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Failure 500 {object} ApiError
// @Router /backend/device [patch]
// @Security ApiBasicAuth
func (s *ApiServer) PatchDevice(c *gin.Context) {
patch, err := c.GetRawData()
if err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return
}
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device")))
if deviceName == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
return
}
// validate device name
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
return
}
device := s.s.peers.GetDevice(deviceName)
if !device.IsValid() {
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
return
}
deviceData, err := json.Marshal(device)
if err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
mergedDeviceData, err := jsonpatch.MergePatch(deviceData, patch)
var mergedDevice wireguard.Device
err = json.Unmarshal(mergedDeviceData, &mergedDevice)
if err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
if !mergedDevice.IsValid() {
c.JSON(http.StatusBadRequest, ApiError{Message: "invalid device model"})
return
}
// Changing device name is not allowed
if deviceName != mergedDevice.DeviceName {
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must match the model device name"})
return
}
// TODO: implement
c.JSON(http.StatusNotImplemented, device)
}
type PeerDeploymentInformation struct {
PublicKey string
Identifier string
Device string
DeviceIdentifier string
}
// GetPeerDeploymentInformation godoc
// @Tags Provisioning
// @Summary Retrieves all active peers for the given email address
// @Produce json
// @Param email query string true "Email Address"
// @Success 200 {object} []PeerDeploymentInformation "All active WireGuard peers"
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Router /provisioning/peers [get]
// @Security GeneralBasicAuth
func (s *ApiServer) GetPeerDeploymentInformation(c *gin.Context) {
email := c.Query("email")
if email == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
return
}
// Get authenticated user to check permissions
username, _, _ := c.Request.BasicAuth()
user := s.s.users.GetUser(username)
if !user.IsAdmin && user.Email != email {
c.JSON(http.StatusForbidden, ApiError{Message: "not enough permissions to access this resource"})
return
}
peers := s.s.peers.GetPeersByMail(email)
result := make([]PeerDeploymentInformation, 0, len(peers))
for i := range peers {
if peers[i].DeactivatedAt != nil {
continue // skip deactivated peers
}
device := s.s.peers.GetDevice(peers[i].DeviceName)
if device.Type != wireguard.DeviceTypeServer {
continue // Skip peers on non-server devices
}
result = append(result, PeerDeploymentInformation{
PublicKey: peers[i].PublicKey,
Identifier: peers[i].Identifier,
Device: device.DeviceName,
DeviceIdentifier: device.DisplayName,
})
}
c.JSON(http.StatusOK, result)
}
// GetPeerDeploymentConfig godoc
// @Tags Provisioning
// @Summary Retrieves the peer config for the given public key
// @Produce plain
// @Param pkey query string true "Public Key (Base 64)"
// @Success 200 {object} string "The WireGuard configuration file"
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Router /provisioning/peer [get]
// @Security GeneralBasicAuth
func (s *ApiServer) GetPeerDeploymentConfig(c *gin.Context) {
pkey := c.Query("pkey")
if pkey == "" {
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
return
}
peer := s.s.peers.GetPeerByKey(pkey)
if !peer.IsValid() {
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
return
}
// Get authenticated user to check permissions
username, _, _ := c.Request.BasicAuth()
user := s.s.users.GetUser(username)
if !user.IsAdmin && user.Email != peer.Email {
c.JSON(http.StatusForbidden, ApiError{Message: "not enough permissions to access this resource"})
return
}
device := s.s.peers.GetDevice(peer.DeviceName)
config, err := peer.GetConfigFile(device)
if err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
c.Data(http.StatusOK, "text/plain", config)
}
type ProvisioningRequest struct {
// DeviceName is optional, if not specified, the configured default device will be used.
DeviceName string `json:",omitempty"`
Identifier string `binding:"required"`
Email string `binding:"required"`
// Client specific and optional settings
AllowedIPsStr string `binding:"cidrlist" json:",omitempty"`
PersistentKeepalive int `binding:"gte=0" json:",omitempty"`
DNSStr string `binding:"iplist" json:",omitempty"`
Mtu int `binding:"gte=0,lte=1500" json:",omitempty"`
}
// PostPeerDeploymentConfig godoc
// @Tags Provisioning
// @Summary Creates the requested peer config and returns the config file
// @Accept json
// @Produce plain
// @Param body body ProvisioningRequest true "Provisioning Request Model"
// @Success 200 {object} string "The WireGuard configuration file"
// @Failure 401 {object} ApiError
// @Failure 403 {object} ApiError
// @Failure 404 {object} ApiError
// @Router /provisioning/peers [post]
// @Security GeneralBasicAuth
func (s *ApiServer) PostPeerDeploymentConfig(c *gin.Context) {
req := ProvisioningRequest{}
if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
return
}
// Get authenticated user to check permissions
username, _, _ := c.Request.BasicAuth()
user := s.s.users.GetUser(username)
if !user.IsAdmin && !s.s.config.Core.SelfProvisioningAllowed {
c.JSON(http.StatusForbidden, ApiError{Message: "peer provisioning service disabled"})
return
}
if !user.IsAdmin && user.Email != req.Email {
c.JSON(http.StatusForbidden, ApiError{Message: "not enough permissions to access this resource"})
return
}
deviceName := req.DeviceName
if deviceName == "" || !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
deviceName = s.s.config.WG.GetDefaultDeviceName()
}
device := s.s.peers.GetDevice(deviceName)
if device.Type != wireguard.DeviceTypeServer {
c.JSON(http.StatusForbidden, ApiError{Message: "invalid device, provisioning disabled"})
return
}
// check if private/public keys are set, if so check database for existing entries
peer, err := s.s.PrepareNewPeer(deviceName)
if err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
peer.Email = req.Email
peer.Identifier = req.Identifier
if req.AllowedIPsStr != "" {
peer.AllowedIPsStr = req.AllowedIPsStr
}
if req.PersistentKeepalive != 0 {
peer.PersistentKeepalive = req.PersistentKeepalive
}
if req.DNSStr != "" {
peer.DNSStr = req.DNSStr
}
if req.Mtu != 0 {
peer.Mtu = req.Mtu
}
if err := s.s.CreatePeer(deviceName, peer); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
config, err := peer.GetConfigFile(device)
if err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
return
}
c.Data(http.StatusOK, "text/plain", config)
}

View File

@@ -64,6 +64,7 @@ type Config struct {
AdminPassword string `yaml:"adminPass" envconfig:"ADMIN_PASS"` AdminPassword string `yaml:"adminPass" envconfig:"ADMIN_PASS"`
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"`
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"`
} `yaml:"core"` } `yaml:"core"`
@@ -96,15 +97,14 @@ func NewConfig() *Config {
cfg.LDAP.StartTLS = true cfg.LDAP.StartTLS = true
cfg.LDAP.BindUser = "company\\\\ldap_wireguard" cfg.LDAP.BindUser = "company\\\\ldap_wireguard"
cfg.LDAP.BindPass = "SuperSecret" cfg.LDAP.BindPass = "SuperSecret"
cfg.LDAP.Type = "AD"
cfg.LDAP.UserClass = "organizationalPerson"
cfg.LDAP.EmailAttribute = "mail" cfg.LDAP.EmailAttribute = "mail"
cfg.LDAP.FirstNameAttribute = "givenName" cfg.LDAP.FirstNameAttribute = "givenName"
cfg.LDAP.LastNameAttribute = "sn" cfg.LDAP.LastNameAttribute = "sn"
cfg.LDAP.PhoneAttribute = "telephoneNumber" cfg.LDAP.PhoneAttribute = "telephoneNumber"
cfg.LDAP.GroupMemberAttribute = "memberOf" cfg.LDAP.GroupMemberAttribute = "memberOf"
cfg.LDAP.DisabledAttribute = "userAccountControl"
cfg.LDAP.AdminLdapGroup = "CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL" cfg.LDAP.AdminLdapGroup = "CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL"
cfg.LDAP.LoginFilter = "(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))"
cfg.LDAP.SyncFilter = "(&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))"
cfg.WG.DeviceNames = []string{"wg0"} cfg.WG.DeviceNames = []string{"wg0"}
cfg.WG.DefaultDeviceName = "wg0" cfg.WG.DefaultDeviceName = "wg0"
@@ -112,6 +112,8 @@ func NewConfig() *Config {
cfg.WG.ManageIPAddresses = true cfg.WG.ManageIPAddresses = true
cfg.Email.Host = "127.0.0.1" cfg.Email.Host = "127.0.0.1"
cfg.Email.Port = 25 cfg.Email.Port = 25
cfg.Email.Encryption = common.MailEncryptionNone
cfg.Email.AuthType = common.MailAuthPlain
// Load config from file and environment // Load config from file and environment
cfgFile, ok := os.LookupEnv("CONFIG_FILE") cfgFile, ok := os.LookupEnv("CONFIG_FILE")

1531
internal/server/docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,13 @@ import (
"net/http" "net/http"
"strings" "strings"
csrf "github.com/utrack/gin-csrf" "github.com/pkg/errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/authentication" "github.com/h44z/wg-portal/internal/authentication"
"github.com/h44z/wg-portal/internal/users" "github.com/h44z/wg-portal/internal/users"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
csrf "github.com/utrack/gin-csrf"
) )
func (s *Server) GetLogin(c *gin.Context) { func (s *Server) GetLogin(c *gin.Context) {
@@ -54,65 +55,15 @@ func (s *Server) PostLogin(c *gin.Context) {
return return
} }
// Check user database for an matching entry
var loginProvider authentication.AuthProvider
email := ""
user := s.users.GetUser(username) // retrieve active candidate user from db
if user != nil { // existing user
loginProvider = s.auth.GetProvider(string(user.Source))
if loginProvider == nil {
s.GetHandleError(c, http.StatusInternalServerError, "login error", "login provider unavailable")
return
}
authEmail, err := loginProvider.Login(&authentication.AuthContext{
Username: username,
Password: password,
})
if err == nil {
email = authEmail
}
} else { // possible new user
// Check all available auth backends // Check all available auth backends
for _, provider := range s.auth.GetProvidersForType(authentication.AuthProviderTypePassword) { user, err := s.checkAuthentication(username, password)
// try to log in to the given provider
authEmail, err := provider.Login(&authentication.AuthContext{
Username: username,
Password: password,
})
if err != nil {
continue
}
email = authEmail
loginProvider = provider
// create new user in the database (or reactivate him)
userData, err := loginProvider.GetUserModel(&authentication.AuthContext{
Username: email,
})
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "login error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "login error", err.Error())
return return
} }
if err := s.CreateUser(users.User{
Email: userData.Email,
Source: users.UserSource(loginProvider.GetName()),
IsAdmin: userData.IsAdmin,
Firstname: userData.Firstname,
Lastname: userData.Lastname,
Phone: userData.Phone,
}, s.wg.Cfg.GetDefaultDeviceName()); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "login error", "failed to update user data")
return
}
user = s.users.GetUser(username)
break
}
}
// Check if user is authenticated // Check if user is authenticated
if email == "" || loginProvider == nil || user == nil { if user == nil {
c.Redirect(http.StatusSeeOther, "/auth/login?err=authfail") c.Redirect(http.StatusSeeOther, "/auth/login?err=authfail")
return return
} }
@@ -153,3 +104,48 @@ func (s *Server) GetLogout(c *gin.Context) {
} }
c.Redirect(http.StatusSeeOther, "/") c.Redirect(http.StatusSeeOther, "/")
} }
func (s *Server) checkAuthentication(username, password string) (*users.User, error) {
var user *users.User
// Check all available auth backends
for _, provider := range s.auth.GetProvidersForType(authentication.AuthProviderTypePassword) {
// try to log in to the given provider
authEmail, err := provider.Login(&authentication.AuthContext{
Username: username,
Password: password,
})
if err != nil {
continue
}
// Login succeeded
user = s.users.GetUser(authEmail)
if user != nil {
break // user exists, nothing more to do...
}
// create new user in the database (or reactivate him)
userData, err := provider.GetUserModel(&authentication.AuthContext{
Username: username,
})
if err != nil {
return nil, errors.Wrap(err, "failed to get user model")
}
if err := s.CreateUser(users.User{
Email: userData.Email,
Source: users.UserSource(provider.GetName()),
IsAdmin: userData.IsAdmin,
Firstname: userData.Firstname,
Lastname: userData.Lastname,
Phone: userData.Phone,
}, s.wg.Cfg.GetDefaultDeviceName()); err != nil {
return nil, errors.Wrap(err, "failed to update user data")
}
user = s.users.GetUser(authEmail)
break
}
return user, nil
}

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

@@ -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"
@@ -64,6 +65,7 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) {
// Clean list input // Clean list input
formPeer.IPsStr = common.ListToString(common.ParseStringList(formPeer.IPsStr)) formPeer.IPsStr = common.ListToString(common.ParseStringList(formPeer.IPsStr))
formPeer.AllowedIPsStr = common.ListToString(common.ParseStringList(formPeer.AllowedIPsStr)) formPeer.AllowedIPsStr = common.ListToString(common.ParseStringList(formPeer.AllowedIPsStr))
formPeer.AllowedIPsSrvStr = common.ListToString(common.ParseStringList(formPeer.AllowedIPsSrvStr))
disabled := c.PostForm("isdisabled") != "" disabled := c.PostForm("isdisabled") != ""
now := time.Now() now := time.Now()
@@ -121,6 +123,7 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) {
// Clean list input // Clean list input
formPeer.IPsStr = common.ListToString(common.ParseStringList(formPeer.IPsStr)) formPeer.IPsStr = common.ListToString(common.ParseStringList(formPeer.IPsStr))
formPeer.AllowedIPsStr = common.ListToString(common.ParseStringList(formPeer.AllowedIPsStr)) formPeer.AllowedIPsStr = common.ListToString(common.ParseStringList(formPeer.AllowedIPsStr))
formPeer.AllowedIPsSrvStr = common.ListToString(common.ParseStringList(formPeer.AllowedIPsSrvStr))
disabled := c.PostForm("isdisabled") != "" disabled := c.PostForm("isdisabled") != ""
now := time.Now() now := time.Now()
@@ -252,52 +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
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: "wireguard-config.png",
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: "wireguard-config.png",
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
} }
@@ -358,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

@@ -8,7 +8,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/users" "github.com/h44z/wg-portal/internal/users"
csrf "github.com/utrack/gin-csrf" csrf "github.com/utrack/gin-csrf"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -105,19 +104,6 @@ func (s *Server) PostAdminUsersEdit(c *gin.Context) {
return return
} }
if formUser.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(formUser.Password), bcrypt.DefaultCost)
if err != nil {
_ = s.updateFormInSession(c, formUser)
SetFlashMessage(c, "failed to hash admin password", "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/edit?pkey="+urlEncodedKey+"&formerr=bind")
return
}
formUser.Password = string(hashedPassword)
} else {
formUser.Password = currentUser.Password
}
disabled := c.PostForm("isdisabled") != "" disabled := c.PostForm("isdisabled") != ""
if disabled { if disabled {
formUser.DeletedAt = gorm.DeletedAt{ formUser.DeletedAt = gorm.DeletedAt{
@@ -175,15 +161,7 @@ func (s *Server) PostAdminUsersCreate(c *gin.Context) {
return return
} }
if formUser.Password != "" { if formUser.Password == "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(formUser.Password), bcrypt.DefaultCost)
if err != nil {
SetFlashMessage(c, "failed to hash admin password", "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=bind")
return
}
formUser.Password = string(hashedPassword)
} else {
_ = s.updateFormInSession(c, formUser) _ = s.updateFormInSession(c, formUser)
SetFlashMessage(c, "invalid password", "danger") SetFlashMessage(c, "invalid password", "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=create") c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=create")

View File

@@ -1,6 +1,7 @@
package server package server
import ( import (
"strings"
"time" "time"
"github.com/h44z/wg-portal/internal/ldap" "github.com/h44z/wg-portal/internal/ldap"
@@ -30,12 +31,91 @@ 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
s.updateLdapUsers(ldapUsers)
// Disable missing LDAP users
s.disableMissingLdapUsers(ldapUsers)
}
logrus.Info("ldap user synchronization stopped")
}
func (s Server) userChangedInLdap(user *users.User, ldapData *ldap.RawLdapData) bool {
if user.Firstname != ldapData.Attributes[s.config.LDAP.FirstNameAttribute] {
return true
}
if user.Lastname != ldapData.Attributes[s.config.LDAP.LastNameAttribute] {
return true
}
if user.Email != strings.ToLower(ldapData.Attributes[s.config.LDAP.EmailAttribute]) {
return true
}
if user.Phone != ldapData.Attributes[s.config.LDAP.PhoneAttribute] {
return true
}
if user.Source != users.UserSourceLdap {
return true
}
if user.DeletedAt.Valid {
return true
}
ldapAdmin := false
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 false
}
func (s *Server) disableMissingLdapUsers(ldapUsers []ldap.RawLdapData) {
// Disable missing LDAP users
activeUsers := s.users.GetUsers()
for i := range activeUsers {
if activeUsers[i].Source != users.UserSourceLdap {
continue
}
existsInLDAP := false
for j := range ldapUsers {
if activeUsers[i].Email == strings.ToLower(ldapUsers[j].Attributes[s.config.LDAP.EmailAttribute]) {
existsInLDAP = true
break
}
}
if existsInLDAP {
continue
}
// disable all peers for the given user
for _, peer := range s.peers.GetPeersByMail(activeUsers[i].Email) {
now := time.Now()
peer.DeactivatedAt = &now
if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update deactivated peer %s: %v", peer.PublicKey, err)
}
}
if err := s.users.DeleteUser(&activeUsers[i]); err != nil {
logrus.Errorf("failed to delete deactivated user %s in database: %v", activeUsers[i].Email, err)
}
}
}
func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData) {
for i := range ldapUsers { for i := range ldapUsers {
// prefilter if ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute] == "" {
if ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute] == "" || logrus.Tracef("skipping sync of %s, empty email attribute", ldapUsers[i].DN)
ldapUsers[i].Attributes[s.config.LDAP.FirstNameAttribute] == "" ||
ldapUsers[i].Attributes[s.config.LDAP.LastNameAttribute] == "" {
continue continue
} }
@@ -44,27 +124,8 @@ func (s *Server) SyncLdapWithUserDatabase() {
logrus.Errorf("failed to get/create user %s in database: %v", ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute], err) logrus.Errorf("failed to get/create user %s in database: %v", ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute], err)
} }
// check if user should be deactivated // re-enable LDAP user if the user was disabled
ldapDeactivated := false if user.DeletedAt.Valid {
switch s.config.LDAP.Type {
case ldap.TypeActiveDirectory:
ldapDeactivated = ldap.IsActiveDirectoryUserDisabled(ldapUsers[i].Attributes[s.config.LDAP.DisabledAttribute])
case ldap.TypeOpenLDAP:
ldapDeactivated = ldap.IsOpenLdapUserDisabled(ldapUsers[i].Attributes[s.config.LDAP.DisabledAttribute])
}
// check if user has been disabled in ldap, update peers accordingly
if ldapDeactivated != user.DeletedAt.Valid {
if ldapDeactivated {
// disable all peers for the given user
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: %v", peer.PublicKey, err)
}
}
} else {
// enable all peers for the given user // enable all peers for the given user
for _, peer := range s.peers.GetPeersByMail(user.Email) { for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now() now := time.Now()
@@ -74,10 +135,10 @@ func (s *Server) SyncLdapWithUserDatabase() {
} }
} }
} }
}
// Sync attributes from ldap // Sync attributes from ldap
if s.UserChangedInLdap(user, &ldapUsers[i]) { if s.userChangedInLdap(user, &ldapUsers[i]) {
logrus.Debugf("updating ldap user %s", user.Email)
user.Firstname = ldapUsers[i].Attributes[s.config.LDAP.FirstNameAttribute] user.Firstname = ldapUsers[i].Attributes[s.config.LDAP.FirstNameAttribute]
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]
@@ -97,54 +158,6 @@ func (s *Server) SyncLdapWithUserDatabase() {
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
} }
if ldapDeactivated {
if err = s.users.DeleteUser(user); err != nil {
logrus.Errorf("failed to delete deactivated user %s in database: %v", user.Email, err)
continue
} }
} }
} }
}
}
logrus.Info("ldap user synchronization stopped")
}
func (s Server) UserChangedInLdap(user *users.User, ldapData *ldap.RawLdapData) bool {
if user.Firstname != ldapData.Attributes[s.config.LDAP.FirstNameAttribute] {
return true
}
if user.Lastname != ldapData.Attributes[s.config.LDAP.LastNameAttribute] {
return true
}
if user.Email != ldapData.Attributes[s.config.LDAP.EmailAttribute] {
return true
}
if user.Phone != ldapData.Attributes[s.config.LDAP.PhoneAttribute] {
return true
}
ldapDeactivated := false
switch s.config.LDAP.Type {
case ldap.TypeActiveDirectory:
ldapDeactivated = ldap.IsActiveDirectoryUserDisabled(ldapData.Attributes[s.config.LDAP.DisabledAttribute])
case ldap.TypeOpenLDAP:
ldapDeactivated = ldap.IsOpenLdapUserDisabled(ldapData.Attributes[s.config.LDAP.DisabledAttribute])
}
if ldapDeactivated != user.DeletedAt.Valid {
return true
}
ldapAdmin := false
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 false
}

View File

@@ -2,12 +2,25 @@ package server
import ( import (
"net/http" "net/http"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
wgportal "github.com/h44z/wg-portal" wgportal "github.com/h44z/wg-portal"
_ "github.com/h44z/wg-portal/internal/server/docs" // docs is generated by Swag CLI, you have to import it.
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
csrf "github.com/utrack/gin-csrf"
) )
func SetupRoutes(s *Server) { func SetupRoutes(s *Server) {
csrfMiddleware := csrf.Middleware(csrf.Options{
Secret: s.config.Core.SessionSecret,
ErrorFunc: func(c *gin.Context) {
c.String(400, "CSRF token mismatch")
c.Abort()
},
})
// Startpage // Startpage
s.server.GET("/", s.GetIndex) s.server.GET("/", s.GetIndex)
s.server.GET("/favicon.ico", func(c *gin.Context) { s.server.GET("/favicon.ico", func(c *gin.Context) {
@@ -21,12 +34,14 @@ func SetupRoutes(s *Server) {
// Auth routes // Auth routes
auth := s.server.Group("/auth") auth := s.server.Group("/auth")
auth.Use(csrfMiddleware)
auth.GET("/login", s.GetLogin) auth.GET("/login", s.GetLogin)
auth.POST("/login", s.PostLogin) auth.POST("/login", s.PostLogin)
auth.GET("/logout", s.GetLogout) auth.GET("/logout", s.GetLogout)
// Admin routes // Admin routes
admin := s.server.Group("/admin") admin := s.server.Group("/admin")
admin.Use(csrfMiddleware)
admin.Use(s.RequireAuthentication("admin")) admin.Use(s.RequireAuthentication("admin"))
admin.GET("/", s.GetAdminIndex) admin.GET("/", s.GetAdminIndex)
admin.GET("/device/edit", s.GetAdminEditInterface) admin.GET("/device/edit", s.GetAdminEditInterface)
@@ -43,6 +58,7 @@ 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)
@@ -52,6 +68,7 @@ func SetupRoutes(s *Server) {
// User routes // User routes
user := s.server.Group("/user") user := s.server.Group("/user")
user.Use(csrfMiddleware)
user.Use(s.RequireAuthentication("")) // empty scope = all logged in users user.Use(s.RequireAuthentication("")) // empty scope = all logged in users
user.GET("/qrcode", s.GetPeerQRCode) user.GET("/qrcode", s.GetPeerQRCode)
user.GET("/profile", s.GetUserIndex) user.GET("/profile", s.GetUserIndex)
@@ -60,6 +77,44 @@ func SetupRoutes(s *Server) {
user.GET("/status", s.GetPeerStatus) user.GET("/status", s.GetPeerStatus)
} }
func SetupApiRoutes(s *Server) {
api := ApiServer{s: s}
// Admin authenticated routes
apiV1Backend := s.server.Group("/api/v1/backend")
apiV1Backend.Use(s.RequireApiAuthentication("admin"))
apiV1Backend.GET("/users", api.GetUsers)
apiV1Backend.POST("/users", api.PostUser)
apiV1Backend.GET("/user", api.GetUser)
apiV1Backend.PUT("/user", api.PutUser)
apiV1Backend.PATCH("/user", api.PatchUser)
apiV1Backend.DELETE("/user", api.DeleteUser)
apiV1Backend.GET("/peers", api.GetPeers)
apiV1Backend.POST("/peers", api.PostPeer)
apiV1Backend.GET("/peer", api.GetPeer)
apiV1Backend.PUT("/peer", api.PutPeer)
apiV1Backend.PATCH("/peer", api.PatchPeer)
apiV1Backend.DELETE("/peer", api.DeletePeer)
apiV1Backend.GET("/devices", api.GetDevices)
apiV1Backend.GET("/device", api.GetDevice)
apiV1Backend.PUT("/device", api.PutDevice)
apiV1Backend.PATCH("/device", api.PatchDevice)
// Simple authenticated routes
apiV1Deployment := s.server.Group("/api/v1/provisioning")
apiV1Deployment.Use(s.RequireApiAuthentication(""))
apiV1Deployment.GET("/peers", api.GetPeerDeploymentInformation)
apiV1Deployment.GET("/peer", api.GetPeerDeploymentConfig)
apiV1Deployment.POST("/peers", api.PostPeerDeploymentConfig)
// Swagger doc/ui
s.server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc { func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
session := GetSessionData(c) session := GetSessionData(c)
@@ -78,7 +133,7 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
return return
} }
// default case if some randome scope was set... // default case if some random scope was set...
if scope != "" && !session.IsAdmin { if scope != "" && !session.IsAdmin {
// Abort the request with the appropriate error code // Abort the request with the appropriate error code
c.Abort() c.Abort()
@@ -86,6 +141,66 @@ 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
c.Next()
}
}
func (s *Server) RequireApiAuthentication(scope string) gin.HandlerFunc {
return func(c *gin.Context) {
username, password, hasAuth := c.Request.BasicAuth()
if !hasAuth {
c.Abort()
c.JSON(http.StatusUnauthorized, ApiError{Message: "unauthorized"})
return
}
// Validate form input
if strings.Trim(username, " ") == "" || strings.Trim(password, " ") == "" {
c.Abort()
c.JSON(http.StatusUnauthorized, ApiError{Message: "unauthorized"})
return
}
// Check all available auth backends
user, err := s.checkAuthentication(username, password)
if err != nil {
c.Abort()
c.JSON(http.StatusInternalServerError, ApiError{Message: "login error"})
return
}
// Check if user is authenticated
if user == nil {
c.Abort()
c.JSON(http.StatusUnauthorized, ApiError{Message: "unauthorized"})
return
}
// Check admin scope
if scope == "admin" && !user.IsAdmin {
// Abort the request with the appropriate error code
c.Abort()
c.JSON(http.StatusForbidden, ApiError{Message: "unauthorized"})
return
}
// default case if some random scope was set...
if scope != "" && !user.IsAdmin {
// Abort the request with the appropriate error code
c.Abort()
c.JSON(http.StatusForbidden, ApiError{Message: "unauthorized"})
return
}
// Continue down the chain to handler etc // Continue down the chain to handler etc
c.Next() c.Next()
} }

View File

@@ -26,7 +26,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
ginlogrus "github.com/toorop/gin-logrus" ginlogrus "github.com/toorop/gin-logrus"
csrf "github.com/utrack/gin-csrf"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -69,6 +68,7 @@ type StaticData struct {
WebsiteLogo string WebsiteLogo string
CompanyName string CompanyName string
Year int Year int
Version string
} }
type Server struct { type Server struct {
@@ -103,7 +103,7 @@ func (s *Server) Setup(ctx context.Context) error {
if err != nil { if err != nil {
return errors.WithMessage(err, "database setup failed") return errors.WithMessage(err, "database setup failed")
} }
err = common.MigrateDatabase(s.db, Version) err = common.MigrateDatabase(s.db, DatabaseVersion)
if err != nil { if err != nil {
return errors.WithMessage(err, "database migration failed") return errors.WithMessage(err, "database migration failed")
} }
@@ -116,14 +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))))
s.server.Use(csrf.Middleware(csrf.Options{ // Authentication cookies
Secret: s.config.Core.SessionSecret, cookieStore := memstore.NewStore([]byte(s.config.Core.SessionSecret))
ErrorFunc: func(c *gin.Context) { cookieStore.Options(sessions.Options{
c.String(400, "CSRF token mismatch") Path: "/",
c.Abort() 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,
@@ -150,6 +152,7 @@ func (s *Server) Setup(ctx context.Context) error {
// Setup all routes // Setup all routes
SetupRoutes(s) SetupRoutes(s)
SetupApiRoutes(s)
// Setup user database (also needed for database authentication) // Setup user database (also needed for database authentication)
s.users, err = users.NewManager(s.db) s.users, err = users.NewManager(s.db)
@@ -253,6 +256,7 @@ func (s *Server) getStaticData() StaticData {
WebsiteLogo: "/img/header-logo.png", WebsiteLogo: "/img/header-logo.png",
CompanyName: s.config.Core.CompanyName, CompanyName: s.config.Core.CompanyName,
Year: time.Now().Year(), Year: time.Now().Year(),
Version: Version,
} }
} }

View File

@@ -12,6 +12,7 @@ import (
"github.com/h44z/wg-portal/internal/wireguard" "github.com/h44z/wg-portal/internal/wireguard"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -52,6 +53,7 @@ func (s *Server) PrepareNewPeer(device string) (wireguard.Peer, error) {
peer.PersistentKeepalive = dev.DefaultPersistentKeepalive peer.PersistentKeepalive = dev.DefaultPersistentKeepalive
peer.AllowedIPsStr = dev.DefaultAllowedIPsStr peer.AllowedIPsStr = dev.DefaultAllowedIPsStr
peer.Mtu = dev.Mtu peer.Mtu = dev.Mtu
peer.DeviceName = device
case wireguard.DeviceTypeClient: case wireguard.DeviceTypeClient:
peer.UID = "newendpoint" peer.UID = "newendpoint"
} }
@@ -225,6 +227,15 @@ func (s *Server) CreateUser(user users.User, device string) error {
return s.UpdateUser(user) return s.UpdateUser(user)
} }
// Hash user password (if set)
if user.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
return errors.Wrap(err, "unable to hash password")
}
user.Password = users.PrivateString(hashedPassword)
}
// Create user in database // Create user in database
if err := s.users.CreateUser(&user); err != nil { if err := s.users.CreateUser(&user); err != nil {
return errors.WithMessage(err, "failed to create user in manager") return errors.WithMessage(err, "failed to create user in manager")
@@ -243,6 +254,17 @@ func (s *Server) UpdateUser(user users.User) error {
currentUser := s.users.GetUserUnscoped(user.Email) currentUser := s.users.GetUserUnscoped(user.Email)
// Hash user password (if set)
if user.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
return errors.Wrap(err, "unable to hash password")
}
user.Password = users.PrivateString(hashedPassword)
} else {
user.Password = currentUser.Password // keep current password
}
// Update in database // Update in database
if err := s.users.UpdateUser(&user); err != nil { if err := s.users.UpdateUser(&user); err != nil {
return errors.WithMessage(err, "failed to update user in manager") return errors.WithMessage(err, "failed to update user in manager")
@@ -287,6 +309,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 {
@@ -294,19 +321,27 @@ 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 { // Create default vpn peer if len(peers) != 0 {
if err := s.CreatePeer(device, wireguard.Peer{ return nil
Identifier: existingUser.Firstname + " " + existingUser.Lastname + " (Default)", }
Email: existingUser.Email,
CreatedBy: existingUser.Email, // Create default vpn peer
UpdatedBy: existingUser.Email, peer, err := s.PrepareNewPeer(device)
}); err != nil { if err != nil {
return errors.WithMessage(err, "failed to prepare new peer")
}
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 errors.WithMessagef(err, "failed to automatically create vpn peer for %s", email)
} }
}
}
return nil return nil
} }

View File

@@ -1,3 +1,4 @@
package server package server
var Version = "1.0.5" var Version = "testbuild"
var DatabaseVersion = "1.0.8"

View File

@@ -51,6 +51,8 @@ func (m Manager) UserExists(email string) bool {
} }
func (m Manager) GetUser(email string) *User { func (m Manager) GetUser(email string) *User {
email = strings.ToLower(email)
user := User{} user := User{}
m.db.Where("email = ?", email).First(&user) m.db.Where("email = ?", email).First(&user)
@@ -62,6 +64,8 @@ func (m Manager) GetUser(email string) *User {
} }
func (m Manager) GetUserUnscoped(email string) *User { func (m Manager) GetUserUnscoped(email string) *User {
email = strings.ToLower(email)
user := User{} user := User{}
m.db.Unscoped().Where("email = ?", email).First(&user) m.db.Unscoped().Where("email = ?", email).First(&user)
@@ -93,6 +97,8 @@ func (m Manager) GetFilteredAndSortedUsersUnscoped(sortKey, sortDirection, searc
} }
func (m Manager) GetOrCreateUser(email string) (*User, error) { func (m Manager) GetOrCreateUser(email string) (*User, error) {
email = strings.ToLower(email)
user := User{} user := User{}
m.db.Where("email = ?", email).FirstOrInit(&user) m.db.Where("email = ?", email).FirstOrInit(&user)
@@ -113,6 +119,8 @@ func (m Manager) GetOrCreateUser(email string) (*User, error) {
} }
func (m Manager) GetOrCreateUserUnscoped(email string) (*User, error) { func (m Manager) GetOrCreateUserUnscoped(email string) (*User, error) {
email = strings.ToLower(email)
user := User{} user := User{}
m.db.Unscoped().Where("email = ?", email).FirstOrInit(&user) m.db.Unscoped().Where("email = ?", email).FirstOrInit(&user)
@@ -133,6 +141,8 @@ func (m Manager) GetOrCreateUserUnscoped(email string) (*User, error) {
} }
func (m Manager) CreateUser(user *User) error { func (m Manager) CreateUser(user *User) error {
user.Email = strings.ToLower(user.Email)
user.Source = UserSourceDatabase
res := m.db.Create(user) res := m.db.Create(user)
if res.Error != nil { if res.Error != nil {
return errors.Wrapf(res.Error, "failed to create user %s", user.Email) return errors.Wrapf(res.Error, "failed to create user %s", user.Email)
@@ -142,6 +152,7 @@ func (m Manager) CreateUser(user *User) error {
} }
func (m Manager) UpdateUser(user *User) error { func (m Manager) UpdateUser(user *User) error {
user.Email = strings.ToLower(user.Email)
res := m.db.Save(user) res := m.db.Save(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)
@@ -151,6 +162,7 @@ func (m Manager) UpdateUser(user *User) error {
} }
func (m Manager) DeleteUser(user *User) error { func (m Manager) DeleteUser(user *User) error {
user.Email = strings.ToLower(user.Email)
res := m.db.Delete(user) res := m.db.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)
@@ -200,7 +212,7 @@ func filterUsers(users []User, search string) []User {
filteredUsers := make([]User, 0, len(users)) filteredUsers := make([]User, 0, len(users))
for i := range users { for i := range users {
if strings.Contains(users[i].Email, search) || if strings.Contains(users[i].Email, strings.ToLower(search)) ||
strings.Contains(users[i].Firstname, search) || strings.Contains(users[i].Firstname, search) ||
strings.Contains(users[i].Lastname, search) || strings.Contains(users[i].Lastname, search) ||
strings.Contains(string(users[i].Source), search) || strings.Contains(string(users[i].Source), search) ||

View File

@@ -14,6 +14,16 @@ const (
UserSourceOIDC UserSource = "oidc" // open id connect, TODO: implement UserSourceOIDC UserSource = "oidc" // open id connect, TODO: implement
) )
type PrivateString string
func (PrivateString) MarshalJSON() ([]byte, error) {
return []byte(`""`), nil
}
func (PrivateString) String() string {
return ""
}
// User is the user model that gets linked to peer entries, by default an empty usermodel with only the email address is created // User is the user model that gets linked to peer entries, by default an empty usermodel with only the email address is created
type User struct { type User struct {
// required fields // required fields
@@ -27,10 +37,10 @@ type User struct {
Phone string `form:"phone" binding:"omitempty"` Phone string `form:"phone" binding:"omitempty"`
// optional, integrated password authentication // optional, integrated password authentication
Password string `form:"password" binding:"omitempty"` Password PrivateString `form:"password" binding:"omitempty"`
// database internal fields // database internal fields
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index" json:",omitempty"`
} }

View File

@@ -4,7 +4,7 @@ import "github.com/h44z/wg-portal/internal/common"
type Config struct { type Config struct {
DeviceNames []string `yaml:"devices" envconfig:"WG_DEVICES"` // managed devices DeviceNames []string `yaml:"devices" envconfig:"WG_DEVICES"` // managed devices
DefaultDeviceName string `yaml:"devices" envconfig:"WG_DEFAULT_DEVICE"` // this device is used for auto-created peers, use GetDefaultDeviceName() to access this field DefaultDeviceName string `yaml:"defaultDevice" envconfig:"WG_DEFAULT_DEVICE"` // this device is used for auto-created peers, use GetDefaultDeviceName() to access this field
ConfigDirectoryPath string `yaml:"configDirectory" envconfig:"WG_CONFIG_PATH"` // optional, if set, updates will be written to this path, filename: <devicename>.conf ConfigDirectoryPath string `yaml:"configDirectory" envconfig:"WG_CONFIG_PATH"` // optional, if set, updates will be written to this path, filename: <devicename>.conf
ManageIPAddresses bool `yaml:"manageIPAddresses" envconfig:"MANAGE_IPS"` // handle ip-address setup of interface ManageIPAddresses bool `yaml:"manageIPAddresses" envconfig:"MANAGE_IPS"` // handle ip-address setup of interface
} }

View File

@@ -4,7 +4,6 @@ import (
"sync" "sync"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )

View File

@@ -4,9 +4,8 @@ import (
"fmt" "fmt"
"net" "net"
"github.com/pkg/errors"
"github.com/milosgajdos/tenus" "github.com/milosgajdos/tenus"
"github.com/pkg/errors"
) )
const DefaultMTU = 1420 const DefaultMTU = 1420

View File

@@ -13,7 +13,6 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/h44z/wg-portal/internal/common" "github.com/h44z/wg-portal/internal/common"
@@ -64,26 +63,26 @@ func init() {
// //
type Peer struct { type Peer struct {
Peer *wgtypes.Peer `gorm:"-"` // WireGuard peer Peer *wgtypes.Peer `gorm:"-" json:"-"` // WireGuard peer
Device *Device `gorm:"foreignKey:DeviceName" binding:"-"` // linked WireGuard device Config string `gorm:"-" json:"-"`
Config string `gorm:"-"`
UID string `form:"uid" binding:"required,alphanum"` // uid for html identification UID string `form:"uid" binding:"required,alphanum" json:"-"` // 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"` DeviceType DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server" json:"-"`
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"`
IsOnline bool `gorm:"-"` IsOnline bool `gorm:"-" json:"-"`
IsNew bool `gorm:"-"` IsNew bool `gorm:"-" json:"-"`
LastHandshake string `gorm:"-"` LastHandshake string `gorm:"-" json:"-"`
LastHandshakeTime string `gorm:"-"` LastHandshakeTime string `gorm:"-" json:"-"`
// Core WireGuard Settings // Core WireGuard Settings
PublicKey string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"` // the public key of the peer itself PublicKey string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"` // the public key of the peer itself
PresharedKey string `form:"presharedkey" binding:"omitempty,base64"` PresharedKey string `form:"presharedkey" binding:"omitempty,base64"`
AllowedIPsStr string `form:"allowedip" binding:"cidrlist"` // a comma separated list of IPs that are used in the client config file AllowedIPsStr string `form:"allowedip" binding:"cidrlist"` // a comma separated list of IPs that are used in the client config file
AllowedIPsSrvStr string `form:"allowedipSrv" binding:"cidrlist"` // a comma separated list of IPs that are used in the server config file
Endpoint string `form:"endpoint" binding:"omitempty,hostname_port"` Endpoint string `form:"endpoint" binding:"omitempty,hostname_port"`
PersistentKeepalive int `form:"keepalive" binding:"gte=0"` PersistentKeepalive int `form:"keepalive" binding:"gte=0"`
@@ -94,7 +93,7 @@ type Peer struct {
// Global Device Settings (can be ignored, only make sense if device is in server mode) // Global Device Settings (can be ignored, only make sense if device is in server mode)
Mtu int `form:"mtu" binding:"gte=0,lte=1500"` Mtu int `form:"mtu" binding:"gte=0,lte=1500"`
DeactivatedAt *time.Time DeactivatedAt *time.Time `json:",omitempty"`
CreatedBy string CreatedBy string
UpdatedBy string UpdatedBy string
CreatedAt time.Time CreatedAt time.Time
@@ -125,7 +124,11 @@ func (p Peer) GetAllowedIPs() []string {
return common.ParseStringList(p.AllowedIPsStr) return common.ParseStringList(p.AllowedIPsStr)
} }
func (p Peer) GetConfig(_ *Device) wgtypes.PeerConfig { func (p Peer) GetAllowedIPsSrv() []string {
return common.ParseStringList(p.AllowedIPsSrvStr)
}
func (p Peer) GetConfig(dev *Device) wgtypes.PeerConfig {
publicKey, _ := wgtypes.ParseKey(p.PublicKey) publicKey, _ := wgtypes.ParseKey(p.PublicKey)
var presharedKey *wgtypes.Key var presharedKey *wgtypes.Key
@@ -135,7 +138,7 @@ func (p Peer) GetConfig(_ *Device) wgtypes.PeerConfig {
} }
var endpoint *net.UDPAddr var endpoint *net.UDPAddr
if p.Endpoint != "" { if p.Endpoint != "" && dev.Type == DeviceTypeClient {
addr, err := net.ResolveUDPAddr("udp", p.Endpoint) addr, err := net.ResolveUDPAddr("udp", p.Endpoint)
if err == nil { if err == nil {
endpoint = addr endpoint = addr
@@ -148,12 +151,19 @@ func (p Peer) GetConfig(_ *Device) wgtypes.PeerConfig {
keepAlive = &keepAliveDuration keepAlive = &keepAliveDuration
} }
peerAllowedIPs := p.GetAllowedIPs() allowedIPs := make([]net.IPNet, 0)
allowedIPs := make([]net.IPNet, len(peerAllowedIPs)) var peerAllowedIPs []string
for i, ip := range peerAllowedIPs { switch dev.Type {
case DeviceTypeClient:
peerAllowedIPs = p.GetAllowedIPs()
case DeviceTypeServer:
peerAllowedIPs = p.GetIPAddresses()
peerAllowedIPs = append(peerAllowedIPs, p.GetAllowedIPsSrv()...)
}
for _, ip := range peerAllowedIPs {
_, ipNet, err := net.ParseCIDR(ip) _, ipNet, err := net.ParseCIDR(ip)
if err == nil { if err == nil {
allowedIPs[i] = *ipNet allowedIPs = append(allowedIPs, *ipNet)
} }
} }
@@ -187,12 +197,21 @@ func (p Peer) GetConfigFile(device Device) ([]byte, error) {
func (p Peer) GetQRCode() ([]byte, error) { func (p Peer) GetQRCode() ([]byte, error) {
png, err := qrcode.Encode(p.Config, qrcode.Medium, 250) png, err := qrcode.Encode(p.Config, qrcode.Medium, 250)
if err != nil { if err == nil {
logrus.WithFields(logrus.Fields{ return png, nil
"err": err, }
}).Error("failed to create qrcode")
if err.Error() != "content too long to encode" {
logrus.Errorf("failed to create qrcode: %v", err)
return nil, errors.Wrap(err, "failed to encode qrcode") return nil, errors.Wrap(err, "failed to encode qrcode")
} }
png, err = qrcode.Encode(p.Config, qrcode.Low, 250)
if err != nil {
logrus.Errorf("failed to create qrcode: %v", err)
return nil, errors.Wrap(err, "failed to encode qrcode")
}
return png, nil return png, nil
} }
@@ -221,7 +240,8 @@ const (
) )
type Device struct { type Device struct {
Interface *wgtypes.Device `gorm:"-"` Interface *wgtypes.Device `gorm:"-" json:"-"`
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,alphanum"`
@@ -361,7 +381,7 @@ func NewPeerManager(db *gorm.DB, wg *Manager) (*PeerManager, error) {
} }
} }
if err := pm.db.AutoMigrate(&Peer{}, &Device{}); err != nil { if err := pm.db.AutoMigrate(&Device{}, &Peer{}); err != nil {
return nil, errors.WithMessage(err, "failed to migrate peer database") return nil, errors.WithMessage(err, "failed to migrate peer database")
} }
@@ -605,7 +625,7 @@ func (m *PeerManager) GetFilteredAndSortedPeers(device, sortKey, sortDirection,
m.populatePeerData(&peers[i]) m.populatePeerData(&peers[i])
if search == "" || if search == "" ||
strings.Contains(peers[i].Email, search) || strings.Contains(peers[i].Email, strings.ToLower(search)) ||
strings.Contains(peers[i].Identifier, search) || strings.Contains(peers[i].Identifier, search) ||
strings.Contains(peers[i].PublicKey, search) { strings.Contains(peers[i].PublicKey, search) {
filteredPeers = append(filteredPeers, peers[i]) filteredPeers = append(filteredPeers, peers[i])
@@ -618,6 +638,7 @@ func (m *PeerManager) GetFilteredAndSortedPeers(device, sortKey, sortDirection,
} }
func (m *PeerManager) GetSortedPeersForEmail(sortKey, sortDirection, email string) []Peer { func (m *PeerManager) GetSortedPeersForEmail(sortKey, sortDirection, email string) []Peer {
email = strings.ToLower(email)
peers := make([]Peer, 0) peers := make([]Peer, 0)
m.db.Where("email = ?", email).Find(&peers) m.db.Where("email = ?", email).Find(&peers)
@@ -686,6 +707,7 @@ func (m *PeerManager) GetPeerByKey(publicKey string) Peer {
} }
func (m *PeerManager) GetPeersByMail(mail string) []Peer { func (m *PeerManager) GetPeersByMail(mail string) []Peer {
mail = strings.ToLower(mail)
var peers []Peer var peers []Peer
m.db.Where("email = ?", mail).Find(&peers) m.db.Where("email = ?", mail).Find(&peers)
for i := range peers { for i := range peers {
@@ -701,6 +723,7 @@ func (m *PeerManager) CreatePeer(peer Peer) error {
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey))) peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
peer.UpdatedAt = time.Now() peer.UpdatedAt = time.Now()
peer.CreatedAt = time.Now() peer.CreatedAt = time.Now()
peer.Email = strings.ToLower(peer.Email)
res := m.db.Create(&peer) res := m.db.Create(&peer)
if res.Error != nil { if res.Error != nil {
@@ -713,6 +736,7 @@ func (m *PeerManager) CreatePeer(peer Peer) error {
func (m *PeerManager) UpdatePeer(peer Peer) error { func (m *PeerManager) UpdatePeer(peer Peer) error {
peer.UpdatedAt = time.Now() peer.UpdatedAt = time.Now()
peer.Email = strings.ToLower(peer.Email)
res := m.db.Save(&peer) res := m.db.Save(&peer)
if res.Error != nil { if res.Error != nil {

View File

@@ -61,7 +61,7 @@ PublicKey = {{ .PublicKey }}
PresharedKey = {{ .PresharedKey }} PresharedKey = {{ .PresharedKey }}
{{- end}} {{- end}}
{{- if eq $.Interface.Type "server"}} {{- if eq $.Interface.Type "server"}}
AllowedIPs = {{ .IPsStr }} AllowedIPs = {{ .IPsStr }}{{if ne .AllowedIPsSrvStr ""}}, {{ .AllowedIPsSrvStr }}{{end}}
{{- end}} {{- end}}
{{- if eq $.Interface.Type "client"}} {{- if eq $.Interface.Type "client"}}
{{- if .AllowedIPsStr}} {{- if .AllowedIPsStr}}

View File

@@ -1,7 +0,0 @@
#!/bin/bash
set -e
goss -g /app/goss/wgportal/goss.yaml validate --format json_oneline
exit 0

View File

@@ -1,3 +0,0 @@
process:
wgportal:
running: true

View File

@@ -1,3 +0,0 @@
process:
wgportal:
running: true