Compare commits

...

54 Commits

Author SHA1 Message Date
Christoph Haas
8de4da8984 Fix circle-ci config 2023-02-19 00:10:49 +01:00
Christoph Haas
4b5e63c44b Update Go version to 1.18, prepare release 2023-02-19 00:07:38 +01:00
Christoph Haas
c5c6135793 Update dependencies 2023-02-18 23:55:32 +01:00
Christoph Haas
105fa8a880 Use Go DNS resolver (#149) 2023-02-13 11:02:06 +01:00
Christoph Haas
3c2c7f325b keep original admin group behaviour 2023-01-06 00:03:37 +01:00
Christoph Haas
1c97ff8d27 remove log entries that contain user input (#140) 2023-01-05 23:21:22 +01:00
Dmitriy
c0879a379f Healthcheck listen on localhost (#138) 2022-12-28 23:39:30 +01:00
Konstantin
112433e87a Fix: "host" network_mode is incompatible with port_bindings (#137)
Removed port bindings.
2022-12-28 11:57:02 +01:00
Christoph Haas
53a6602a64 cleanup recursive ldap group sync 2022-12-27 13:36:25 +01:00
Christoph Haas
f2afd4a21c Merge branch 'asterix11-master' 2022-12-27 12:36:23 +01:00
Fabian Schultis
a2ab5c9301 Fix nested LDAP group resolution 2022-12-07 05:01:06 +01:00
Fabian Schultis
6f463ac9a5 Add nested group admin state resolution 2022-12-06 02:59:29 +01:00
Christoph Haas
fda3e7b2be fix makefile (#132) 2022-12-04 21:06:41 +01:00
Christoph Haas
dab1e13c54 fix circle ci config 2022-11-11 19:18:10 +01:00
Christoph Haas
51fb9b4139 cleanup code warnings, update RaspberryPi readme 2022-11-11 18:17:38 +01:00
Christoph Haas
bda8c9a3d1 fix migration issue for mysql/mariadb (#128) 2022-11-11 18:07:48 +01:00
h44z
54716f7f53 Multiarch Docker Build (#104) (#129)
* Improved Makefile
* Multiarch Docker build (amd64, arm64 and armv7)
* closes #104
2022-11-11 17:10:41 +01:00
Christoph Haas
e97fb38bd5 fix issue where newly created peers expire 2022-11-08 18:02:00 +01:00
Christoph Haas
2796433973 expiry feature: automatically re-enable peers if date is in the future 2022-11-01 10:51:17 +01:00
Christoph Haas
3e2208c8f6 ensure that db index is re-created (avoids invalid DDL errors), update gorm 2022-10-29 15:24:13 +02:00
Christoph Haas
09a9af245c prepare new release 2022-10-29 14:27:56 +02:00
h44z
979cec7d83 Merge pull request #127 from h44z/feat_exp
Expiry Date for Peers
2022-10-29 14:20:36 +02:00
Christoph Haas
0f33871850 peer expiry feature: update api docs and readme 2022-10-29 13:18:32 +02:00
Christoph Haas
c43e8d7ca2 peer expiry feature: re-activate expired peers 2022-10-29 13:03:05 +02:00
Christoph Haas
4a0e773d96 peer expiry feature: expiration check 2022-10-29 11:21:04 +02:00
Christoph Haas
6f4af97024 peer expiry feature: frontend updates 2022-10-29 10:12:42 +02:00
Christoph Haas
0d5b895174 lazy load qr code (if browser supports it) 2022-10-29 10:06:58 +02:00
Christoph Haas
fe3247bdc1 peer expiry feature: database model, frontend updates 2022-10-28 23:21:37 +02:00
Christoph Haas
e4b927bc45 use go-playground/validator instead of asaskevich/govalidator (#46) 2022-10-28 21:48:44 +02:00
philippderdiedas
383fc8cb58 Merge branch 'h44z:master' into master 2022-10-28 20:42:35 +02:00
Christoph Haas
ab7f19bb55 only remove private key if a custom public key was specified (#112) 2022-10-28 18:40:06 +02:00
Philipp Harms
49c7109c61 Fix DNSStr validator 2022-10-28 18:31:20 +02:00
Fabian Schultis
352c689623 Remove as in https://github.com/h44z/wg-portal/issues/112 2022-10-28 17:54:13 +02:00
skodapilot
e6a8e2f2cf Fixed possibility to save clients without preshared key (#114) 2022-09-19 22:39:34 +02:00
dada513
12717987a6 Add config option to make everyone admin (#106) 2022-09-19 22:26:11 +02:00
Christoph Haas
e4c641f78f update dependencies 2022-05-26 23:29:07 +02:00
Alexander Beck
2f194884d3 user can manage own peers on default device (#82)
Co-authored-by: GitHubActionRunner <knm@knm.io>
2022-05-26 23:10:17 +02:00
Christoph Haas
b34d2e1174 fix status code number (related to #95) 2022-04-30 19:43:39 +02:00
lowid
a46e3724bf fix delete peer status code (#95)
http.StatusNoContent == 204
2022-04-30 19:42:46 +02:00
Christoph Haas
83271b5d34 fix user edit bug, allow to delete users from the database (#40) 2022-03-15 23:34:55 +01:00
Alexis
cc50fcf8e6 Feat/ldap certificate connexion (#92)
* Give the way to connect against LDAP server with certificate and key

* fix(ldap) Update cert variable name

In order to be more explicit

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

* add friendly name as option to configuration

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

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

* ldap/isAdmin- restructure & remove code duplication

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

* tests/api - fix NotImplemented

* tests - add README

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

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

* api - match paramters to their property equivalents

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

* api - use ShouldBindJSON instead of BindJSON

 BindJSON sets the content-type text/plain

* api - we renamed, we regenerated

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

* api - more pascalcase & argument renames

* api - marshal DeletedAt as string

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

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

* assets - name forms for use with mechanize

* api - match error message

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

* tests - test address exhaustion

* tests - test network expansion

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

* Fixed inconsistent height of custom logos

* Extended navbar style to login page
2021-09-12 10:17:13 +02:00
53 changed files with 2505 additions and 795 deletions

View File

@@ -1,55 +1,92 @@
version: 2.1
jobs:
build:
working_directory: ~/repo
docker:
- image: circleci/golang:1.16.7
build-latest:
steps:
- checkout
- restore_cache:
keys:
- go-mod-v4-{{ checksum "go.sum" }}
- go-mod-latest-v4-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: |
make dep
make build-dependencies
- save_cache:
key: go-mod-v4-{{ checksum "go.sum" }}
key: go-mod-latest-v4-{{ checksum "go.sum" }}
paths:
- "/go/pkg/mod"
- "~/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
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-amd64
- 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 apt-get update
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-aarch64-linux-gnu libc6-dev-arm64-cross
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
sudo ln -s /usr/include/asm-generic /usr/include/asm
- run:
name: Build ARM64
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-arm64
- 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
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-arm
- store_artifacts:
path: ~/repo/dist
- run:
name: "Publish Release on GitHub"
command: |
if [ ! -z "${CIRCLE_TAG}" ]; then
go get github.com/tcnksm/ghr
go install github.com/tcnksm/ghr@latest
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace $CIRCLE_TAG ~/repo/dist
fi
working_directory: ~/repo
docker:
- image: cimg/go:1.19
build-118: # just to validate compatibility with minimum go version
steps:
- checkout
- restore_cache:
keys:
- go-mod-118-v4-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: |
make build-dependencies
- save_cache:
key: go-mod-118-v4-{{ checksum "go.sum" }}
paths:
- "~/go/pkg/mod"
- run:
name: Build
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build
working_directory: ~/repo118
docker:
- image: cimg/go:1.18
workflows:
build-and-release:
jobs:
#--------------- BUILD ---------------#
- build:
name: build
- build-latest:
filters:
tags:
only: /^v.*/
- build-118:
requires:
- build-latest
filters:
tags:
only: /^v.*/

View File

@@ -24,7 +24,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Get Version
shell: bash
@@ -35,14 +41,14 @@ jobs:
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v4
with:
images: h44z/wg-portal
flavor: |
@@ -55,12 +61,13 @@ jobs:
type=semver,pattern={{version}}
- name: Build and push Docker image
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
build-args: |
BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }}
BUILD_VERSION=${{ steps.get_version.outputs.hash }}
@@ -74,7 +81,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Get Version
shell: bash
@@ -87,7 +100,7 @@ jobs:
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -97,7 +110,7 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
@@ -112,12 +125,13 @@ jobs:
# 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
uses: docker/build-push-action@v3
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
build-args: |
BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }}
BUILD_VERSION=${{ steps.get_version.outputs.hash }}

1
.gitignore vendored
View File

@@ -28,6 +28,7 @@
out/
dist/
data/
docker_images/
ssh.key
.testCoverage.txt
wg_portal.db

View File

@@ -4,7 +4,7 @@
######-
# Start from the latest golang base image as builder image (only used to compile the code)
######-
FROM golang:1.16 as builder
FROM golang:1.18 as builder
ARG BUILD_IDENTIFIER
ENV ENV_BUILD_IDENTIFIER=$BUILD_IDENTIFIER
@@ -12,6 +12,10 @@ ENV ENV_BUILD_IDENTIFIER=$BUILD_IDENTIFIER
ARG BUILD_VERSION
ENV ENV_BUILD_VERSION=$BUILD_VERSION
# populated by BuildKit
ARG TARGETPLATFORM
ENV ENV_TARGETPLATFORM=$TARGETPLATFORM
RUN mkdir /build
# Copy the source from the current directory to the Working Directory inside the container
@@ -20,16 +24,8 @@ ADD . /build/
# Set the Current Working Directory inside the container
WORKDIR /build
# Workaround for failing travis-ci builds
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
RUN go clean -modcache; go mod tidy; make build-docker
RUN echo "Building version '$ENV_BUILD_IDENTIFIER-$ENV_BUILD_VERSION' for platform $ENV_TARGETPLATFORM"; make build
######-
# Here starts the main image
@@ -44,16 +40,14 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Import healthcheck binary
COPY --from=builder /build/hc /app/hc
# Copy binaries
COPY --from=builder /build/dist/wgportal /app/wgportal
COPY --from=builder /build/dist/wg-portal /app/wg-portal
COPY --from=builder /build/dist/hc /app/hc
# Set the Current Working Directory inside the container
WORKDIR /app
# Command to run the executable
CMD [ "/app/wgportal" ]
CMD [ "/app/wg-portal" ]
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD [ "/app/hc", "http://localhost:11223/health" ]

165
Makefile
View File

@@ -6,62 +6,123 @@ BUILDDIR=dist
BINARIES=$(subst cmd/,,$(wildcard cmd/*))
IMAGE=h44z/wg-portal
.PHONY: all test clean phony
all: help
all: dep build
.PHONY: help
help:
@echo "Usage:"
@sed -n 's/^#>//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' # user commands (#>)
@echo ""
@echo "Advanced commands:"
@sed -n 's/^#<//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' # internal commands (#<)
build: dep $(addsuffix -amd64,$(addprefix $(BUILDDIR)/,$(BINARIES)))
cp scripts/wg-portal.service $(BUILDDIR)
cp scripts/wg-portal.env $(BUILDDIR)
########################################################################################
##
## DEVELOPER / USER TARGETS
##
########################################################################################
build-cross-plat: dep build $(addsuffix -arm,$(addprefix $(BUILDDIR)/,$(BINARIES))) $(addsuffix -arm64,$(addprefix $(BUILDDIR)/,$(BINARIES)))
cp scripts/wg-portal.service $(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:
$(GOCMD) mod download
validate: dep
$(GOCMD) fmt $(GOFILES)
$(GOCMD) vet $(GOFILES)
$(GOCMD) test -race $(GOFILES)
coverage: dep
$(GOCMD) fmt $(GOFILES)
$(GOCMD) test $(GOFILES) -v -coverprofile .testCoverage.txt
$(GOCMD) tool cover -func=.testCoverage.txt # use total:\s+\(statements\)\s+(\d+.\d+\%) as Gitlab CI regextotal:\s+\(statements\)\s+(\d+.\d+\%)
coverage-html: coverage
$(GOCMD) tool cover -html=.testCoverage.txt
test: dep
$(GOCMD) test $(MODULENAME)/... -v -count=1
clean:
$(GOCMD) clean $(GOFILES)
rm -rf .testCoverage.txt
rm -rf $(BUILDDIR)
docker-build:
docker build -t $(IMAGE) .
docker-push:
docker push $(IMAGE)
api-docs:
cd internal/server; swag init --parseDependency --parseInternal --generalInfo api.go
#> codegen: Re-generate autogenerated files (like API docs)
.PHONY: codegen
codegen: $(SUBDIRS)
cd internal; swag init --propertyStrategy pascalcase --parseInternal --generalInfo server/api.go --output server/docs/
$(GOCMD) fmt internal/server/docs/docs.go
$(BUILDDIR)/%-amd64: cmd/%/main.go dep phony
GOOS=linux GOARCH=amd64 $(GOCMD) build -ldflags "-X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $<
#> update: Update all dependencies
.PHONY: update
update:
@ $(GOCMD) get -u ./...
@ $(GOCMD) mod tidy
# On arch-linux install aarch64-linux-gnu-gcc to crosscompile for arm64
$(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\" -X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $<
#> format: Re-format the code
.PHONY: format
format:
@echo "Formatting code..."
@ $(GOCMD) fmt $(GOFILES)
# On arch-linux install arm-linux-gnueabihf-gcc to crosscompile for arm
$(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\" -X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $<
########################################################################################
##
## TESTING / CODE QUALITY TARGETS
##
########################################################################################
#> test: Run all kinds of tests, except for integration tests
.PHONY: test
test: test-vet test-race
#< test-vet: Static code analysis
.PHONY: test-vet
test-vet: build-dependencies
@$(GOCMD) vet $(GOFILES)
#< test-race: Race condition test
.PHONY: test-race
test-race: build-dependencies
@$(GOCMD) test -race -short $(GOFILES)
########################################################################################
##
## CI TARGETS
##
########################################################################################
#< clean: Delete all generated executables and test files
.PHONY: clean
clean:
@rm -rf $(BUILDDIR)
#< build: Build all executables (architecture depends on build system)
.PHONY: build
build: build-dependencies
CGO_ENABLED=1 $(GOCMD) build -o $(BUILDDIR)/wg-portal \
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
-tags netgo \
cmd/wg-portal/main.go
CGO_ENABLED=0 $(GOCMD) build -o $(BUILDDIR)/hc \
-ldflags "-w -s -extldflags \"-static\"" \
cmd/hc/main.go
#< build-amd64: Build all executables for AMD64
.PHONY: build-amd64
build-amd64: build-dependencies
CGO_ENABLED=1 $(GOCMD) build -o $(BUILDDIR)/wg-portal-amd64 \
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
-tags netgo \
cmd/wg-portal/main.go
CGO_ENABLED=0 $(GOCMD) build -o $(BUILDDIR)/hc-amd64 \
-ldflags "-w -s -extldflags \"-static\"" \
cmd/hc/main.go
#< build-arm64: Build all executables for ARM64
.PHONY: build-arm64
build-arm64: build-dependencies
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(GOCMD) build -o $(BUILDDIR)/wg-portal-arm64 \
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
-tags netgo \
cmd/wg-portal/main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOCMD) build -o $(BUILDDIR)/hc-arm64 \
-ldflags "-w -s -extldflags \"-static\"" \
cmd/hc/main.go
#< build-arm: Build all executables for ARM32
.PHONY: build-arm
build-arm: build-dependencies
CGO_ENABLED=1 CC=arm-linux-gnueabi-gcc GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -o $(BUILDDIR)/wg-portal-arm \
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
-tags netgo \
cmd/wg-portal/main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -o $(BUILDDIR)/hc-arm \
-ldflags "-w -s -extldflags \"-static\"" \
cmd/hc/main.go
#< build-dependencies: Generate the output directory for compiled executables and download dependencies
.PHONY: build-dependencies
build-dependencies:
@$(GOCMD) mod download -x
@mkdir -p $(BUILDDIR)
cp scripts/wg-portal.service $(BUILDDIR)
cp scripts/wg-portal.env $(BUILDDIR)

View File

@@ -4,16 +4,21 @@ This readme only contains a detailed explanation of how to set up the WireGuard
## Setup
You can download prebuild binaries from the [release page](https://github.com/h44z/wg-portal/releases). If you want to build the binary yourself,
use the following instructions:
You can either download prebuild binaries from the [release page](https://github.com/h44z/wg-portal/releases) or use Docker images for ARM.
If you want to build the binary yourself, use the following building instructions.
### Building
This section describes how to build the WireGuard Portal code.
To compile the final binary, use the Makefile provided in the repository.
As WireGuard Portal is written in Go, **golang >= 1.16** must be installed prior to building.
As WireGuard Portal is written in Go, **golang >= 1.18** must be installed prior to building.
If you want to cross compile ARM binaries from AMD64 systems, install *arm-linux-gnueabi-gcc* (armv7) or *aarch64-linux-gnu-gcc* (arm64).
```
make build-cross-plat
# for 64 bit OS
make build-arm64
# for 32 bit OS
make build-arm
```
The compiled binary and all necessary assets will be located in the dist folder.

166
README.md
View File

@@ -8,9 +8,9 @@
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/h44z/wg-portal)
[![Docker Pulls](https://img.shields.io/docker/pulls/h44z/wg-portal.svg)](https://hub.docker.com/r/h44z/wg-portal/)
A simple, web based configuration portal for [WireGuard](https://wireguard.com).
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
A simple, web based configuration portal for [WireGuard](https://wireguard.com).
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
connections.
The configuration portal currently supports using SQLite and MySQL as a user source for authentication and profile data.
@@ -31,11 +31,12 @@ It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
* Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces
* REST API for management and client deployment
* Peer Expiry Feature
![Screenshot](screenshot.png)
## Setup
Make sure that your host system has at least one WireGuard interface (for example wg0) available.
Make sure that your host system has at least one WireGuard interface (for example wg0) available.
If you did not start up a WireGuard interface yet, take a look at [wg-quick](https://manpages.debian.org/unstable/wireguard-tools/wg-quick.8.en.html) in order to get started.
### Docker
@@ -84,21 +85,43 @@ services:
```
Please note that mapping ```/etc/wireguard``` to ```/etc/wireguard``` inside the docker, will erase your host's current configuration.
If needed, please make sure to back up your files from ```/etc/wireguard```.
For a full list of configuration options take a look at the source file [internal/server/configuration.go](internal/server/configuration.go#L56).
For a full list of configuration options take a look at the source file [internal/server/configuration.go](internal/server/configuration.go#L58).
### Standalone
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.
For a standalone application, use the Makefile provided in the repository to build the application. Go version 1.18 or higher has to be installed to build WireGuard Portal.
```
```shell
# show all possible make commands
make
# To build for arm architecture as well use:
make build-cross-plat
# build wg-portal for current system architecture
make build
```
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).
To build the Docker image, Docker (> 20.x) with buildx is required. If you want to build cross-platform images, you need to install qemu.
On arch linux for example install: `docker-buildx qemu-user-static qemu-user-static-binfmt`.
Once the Docker setup is completed, create a new buildx builder:
```shell
docker buildx create --name wgportalbuilder --platform linux/arm/v7,linux/arm64,linux/amd64
docker buildx use wgportalbuilder
docker buildx inspect --bootstrap
```
Now you can compile the Docker image:
```shell
# multi platform build, can only be exported to tar archives
docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 --output type=local,dest=docker_images \
--build-arg BUILD_IDENTIFIER=dev --build-arg BUILD_VERSION=0.1 -t h44z/wg-portal .
# image for current platform only (same as docker build)
docker buildx build --load \
--build-arg BUILD_IDENTIFIER=dev --build-arg BUILD_VERSION=0.1 -t h44z/wg-portal .
```
## 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.
@@ -108,56 +131,65 @@ For example: `CONFIG_FILE=/home/test/config.yml ./wg-portal-amd64`.
### Configuration Options
The following configuration options are available:
| 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. |
| 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. |
| LOGO_URL | logoUrl | core | /img/header-logo.png | The logo displayed in the page's header. |
| 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. |
| WG_EXPORTER_FRIENDLY_NAMES | wgExporterFriendlyNames | core | false | Enable integration with [prometheus_wireguard_exporter friendly name](https://github.com/MindFlavor/prometheus_wireguard_exporter#friendly-tags). |
| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. |
| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. |
| BACKGROUND_TASK_INTERVAL | backgroundTaskInterval | core | 900 | The interval (in seconds) for the background tasks (like peer expiry check). |
| EXPIRY_REENABLE | expiryReEnable | core | false | Reactivate expired peers if the expiration date is in the future. |
| 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. |
| USER_MANAGE_PEERS | userManagePeers | wg | false | Logged in user can create or update peers (partially). |
| 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. Users matching this filter will be synchronized with the WireGuard Portal database. |
| LDAP_SYNC_GROUP_FILTER | syncGroupFilter | ldap | | The filter string for the LDAP groups, for example: (objectClass=group). The groups are used to recursively check for admin group member ship of users. |
| 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. |
| LDAP_CERT_CONN | ldapCertConn | ldap | false | Allow connection with certificate against LDAP server without user/password |
| LDAPTLS_CERT | ldapTlsCert | ldap | | The LDAP cert's path |
| LDAPTLS_KEY | ldapTlsKey | ldap | | The LDAP key's path |
| LOG_LEVEL | | | debug | Specify log level, one of: trace, debug, info, off. |
| LOG_JSON | | | false | Format log output as JSON. |
| LOG_COLOR | | | true | Colorize log output. |
| CONFIG_FILE | | | config.yml | The config file path. |
### Sample yaml configuration
config.yml:
@@ -188,7 +220,7 @@ email:
user: test@gmail.com
pass: topsecret
wg:
devices:
devices:
- wg0
- wg1
defaultDevice: wg0
@@ -197,16 +229,18 @@ wg:
```
### 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`.
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?displayOperationId=true`.
The [API's unittesting](tests/test_API.py) may serve as an example how to make use of the API with python3 & pyswagger.
## What is out of scope
* 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
* [Gin, HTTP web framework written in Go](https://github.com/gin-gonic/gin)
@@ -217,6 +251,6 @@ under the URL `http://<your wg-portal ip/domain>/swagger/index.html`.
## License
* MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT
This project was inspired by [wg-gen-web](https://github.com/vx3r/wg-gen-web).

View File

@@ -64,10 +64,19 @@ pre{background:#f7f7f9}iframe{overflow:hidden;border:none}@media (min-width: 768
padding: 0.5rem 1rem;
}
.navbar-brand > img {
height: 2rem;
width: auto;
}
.disabled-peer {
color: #d03131;
}
.expiring-peer {
color: #d09d12;
}
.tokenfield .token {
border-radius: 0px;
border: 1px solid #1a1a1a;
@@ -100,4 +109,10 @@ a.advanced-settings.collapsed:before {
.text-blue {
color: #0057bb;
}
@media (min-width: 992px) {
.pull-right-lg {
float: right;
}
}

View File

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

View File

@@ -45,7 +45,7 @@
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_PresharedKey">Preshared Key</label>
<input type="text" name="presharedkey" class="form-control" id="server_PresharedKey" value="{{.Peer.PresharedKey}}" required>
<input type="text" name="presharedkey" class="form-control" id="server_PresharedKey" value="{{.Peer.PresharedKey}}">
</div>
</div>
{{else}}
@@ -106,7 +106,7 @@
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="form-group col-md-6">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}>
<label class="custom-control-label" for="server_Disabled">
@@ -120,6 +120,10 @@
</label>
</div>
</div>
<div class="form-group col-md-6">
<label for="expires_at">Expires At</label>
<input type="date" name="expires_at" pattern="\d{4}-\d{2}-\d{2}" class="form-control" id="expires_at" placeholder="" value="{{formatDate .Peer.ExpiresAt}}" min="2022-01-01">
</div>
</div>
@@ -185,7 +189,7 @@
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="form-group col-md-6">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="client_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}>
<label class="custom-control-label" for="client_Disabled">
@@ -193,6 +197,10 @@
</label>
</div>
</div>
<div class="form-group col-md-6">
<label for="expires_at">Expires At</label>
<input type="date" name="expires_at" pattern="\d{4}-\d{2}-\d{2}" class="form-control" id="expires_at" placeholder="" value="{{formatDate .Peer.ExpiresAt}}" min="2022-01-01">
</div>
</div>

View File

@@ -28,7 +28,7 @@
<div id="configContent" class="tab-content">
<!-- server mode -->
<div class="tab-pane fade {{if eq .Device.Type "server"}}active show{{end}}" id="server">
<form method="post" enctype="multipart/form-data">
<form method="post" enctype="multipart/form-data" name="server">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="devicetype" value="server">
@@ -162,7 +162,7 @@
<!-- client mode -->
<div class="tab-pane fade {{if eq .Device.Type "client"}}active show{{end}}" id="client">
<form method="post" enctype="multipart/form-data">
<form method="post" enctype="multipart/form-data" name="client">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="devicetype" value="client">

View File

@@ -76,6 +76,11 @@
<button type="submit" class="btn btn-primary">Save</button>
<a href="/admin/users/" class="btn btn-secondary">Cancel</a>
{{if eq $.Session.IsAdmin true}}
{{if eq .User.Source "db"}}
<a href="/admin/users/delete?pkey={{.User.Email}}" data-toggle="confirmation" data-title="Really delete user and associated peers?" title="Delete user and associated peers" class="btn btn-danger float-right">Delete</a>
{{end}}
{{end}}
</form>
</div>
{{template "prt_footer.html" .}}

View File

@@ -170,7 +170,7 @@
<!-- online check -->
<span title="Online status" class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
</th>
<td>{{$p.Identifier}}</td>
<td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
<td>{{$p.PublicKey}}</td>
{{if eq $.Device.Type "server"}}
<td>{{$p.Email}}</td>
@@ -239,12 +239,18 @@
</div>
<div class="col-md-3">
{{if eq $.Device.Type "server"}}
<img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
<img class="list-image-large" loading="lazy" alt="Configuration QR Code" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
{{end}}
</div>
<div class="col-md-3">
{{if $p.DeactivatedAt}}
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
{{end}}
{{if $p.WillExpire}}
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Peer will expire on {{ formatDate $p.ExpiresAt}}</div>
{{end}}
{{if eq $.Device.Type "server"}}
<div class="float-right mt-5">
<div class="pull-right-lg mt-lg-5 mt-md-3">
<a href="/admin/peer/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
<a href="/admin/peer/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
</div>

View File

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

View File

@@ -27,11 +27,11 @@
<div class="card mt-5">
<div class="card-header">Please sign in</div>
<div class="card-body">
<form class="form-signin" method="post">
<form class="form-signin" method="post" name="login">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<div class="form-group">
<label for="inputUsername">Email</label>
<input type="text" name="username" class="form-control" id="inputUsername" aria-describedby="usernameHelp" placeholder="Enter email">
<label for="inputUsername">Username</label>
<input type="text" name="username" class="form-control" id="inputUsername" aria-describedby="usernameHelp" placeholder="Enter username or email">
</div>
<div class="form-group">
<label for="inputPassword">Password</label>

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Admin</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5">
{{template "prt_flashes.html" .}}
<!-- server mode -->
<h1>Create a new client</h1>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="uid" value="{{.Peer.UID}}">
<div class="form-row">
<div class="form-group required col-md-12">
<label for="server_PublicKey">Public Key</label>
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/user/profile" class="btn btn-secondary">Cancel</a>
</form>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Admin</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5">
{{template "prt_flashes.html" .}}
<!-- server mode -->
<h1>Edit client: <strong>{{.Peer.Identifier}}</strong></h1>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="uid" value="{{.Peer.UID}}">
<div class="form-row">
<div class="form-group required col-md-12">
<label for="server_PublicKey">Public Key</label>
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required disabled="disabled">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{if .Peer.DeactivatedAt}}disabled="disabled"{{end}} {{if .Peer.DeactivatedAt}}checked{{end}}>
<label class="custom-control-label" for="server_Disabled">
Disabled
</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/user/profile" class="btn btn-secondary">Cancel</a>
</form>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

View File

@@ -15,7 +15,16 @@
<div class="container mt-5">
<h1>WireGuard VPN User-Portal</h1>
<h2 class="mt-4">Your VPN Profiles</h2>
<div class="mt-4 row">
<div class="col-sm-8 col-12">
<h2 class="mt-2">Your VPN Profiles</h2>
</div>
<div class="col-sm-4 col-12 text-right">
{{if eq $.UserManagePeers true}}
<a href="/user/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>
{{end}}
</div>
</div>
<div class="mt-2 table-responsive">
<table class="table table-sm" id="userTable">
<thead>
@@ -26,6 +35,9 @@
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "mail"}}"></i></a></th>
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "ip"}}"></i></a></th>
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "handshake"}}"></i></a></th>
{{if eq $.UserManagePeers true}}
<th scope="col"></th>
{{end}}
</tr>
</thead>
<tbody>
@@ -37,11 +49,16 @@
<!-- online check -->
<span class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
</th>
<td>{{$p.Identifier}}</td>
<td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
<td>{{$p.PublicKey}}</td>
<td>{{$p.Email}}</td>
<td>{{$p.IPsStr}}</td>
<td><span data-toggle="tooltip" data-placement="left" title="" data-original-title="{{$p.LastHandshakeTime}}">{{$p.LastHandshake}}</span></td>
{{if eq $.UserManagePeers true}}
<td>
<a href="/user/peer/edit?pkey={{$p.PublicKey}}" title="Edit peer"><i class="fas fa-cog"></i></a>
</td>
{{end}}
</tr>
<tr class="hiddenRow">
<td colspan="6" class="hiddenCell" style="white-space:nowrap">
@@ -85,7 +102,13 @@
<img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
</div>
<div class="col-md-3">
<div class="float-right mt-5">
{{if $p.DeactivatedAt}}
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
{{end}}
{{if $p.WillExpire}}
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Profile expires on {{ formatDate $p.ExpiresAt}}</div>
{{end}}
<div class="pull-right-lg mt-lg-5 mt-md-3">
<a href="/user/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
<a href="/user/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
</div>

35
cmd/hc/main.go Normal file
View File

@@ -0,0 +1,35 @@
// source taken from https://git.prolicht.digital/golib/healthcheck/-/blob/master/cmd/hc/main.go
package main
import (
"net/http"
"os"
"time"
)
// main checks the given URL, if the response is not 200, it will return with exit code 1
// on success, exit code 0 will be returned
func main() {
os.Exit(checkWebEndpointFromArgs())
}
func checkWebEndpointFromArgs() int {
if len(os.Args) < 2 {
return 1
}
if status := checkWebEndpoint(os.Args[1]); !status {
return 1
}
return 0
}
func checkWebEndpoint(url string) bool {
client := &http.Client{
Timeout: time.Second * 2,
}
if resp, err := client.Get(url); err != nil || resp.StatusCode < 200 || resp.StatusCode > 299 {
return false
}
return true
}

View File

@@ -2,13 +2,14 @@ package main
import (
"context"
"io/ioutil"
"io"
"os"
"os/signal"
"runtime"
"syscall"
"time"
"git.prolicht.digital/pub/healthcheck"
"git.prolicht.digital/golib/healthcheck"
"github.com/h44z/wg-portal/internal/server"
"github.com/sirupsen/logrus"
)
@@ -19,6 +20,7 @@ func main() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
logrus.Infof("sysinfo: os=%s, arch=%s", runtime.GOOS, runtime.GOARCH)
logrus.Infof("starting WireGuard Portal Server [%s]...", server.Version)
// Context for clean shutdown
@@ -26,7 +28,7 @@ func main() {
defer cancel()
// start health check service on port 11223
healthcheck.New(healthcheck.WithContext(ctx)).Start()
healthcheck.New(healthcheck.ListenOn("127.0.0.1:11223")).StartWithContext(ctx)
service := server.Server{}
if err := service.Setup(ctx); err != nil {
@@ -72,7 +74,7 @@ func setupLogger(logger *logrus.Logger) error {
switch level {
case "off":
logger.SetOutput(ioutil.Discard)
logger.SetOutput(io.Discard)
case "info":
logger.SetLevel(logrus.InfoLevel)
case "debug":

View File

@@ -1,16 +1,19 @@
---
version: '3.6'
services:
wg-portal:
image: h44z/wg-portal:1.0.6
image: h44z/wg-portal:1.0.17
container_name: wg-portal
restart: unless-stopped
logging:
options:
max-size: "10m"
max-file: "3"
cap_add:
- NET_ADMIN
network_mode: "host"
volumes:
- /etc/wireguard:/etc/wireguard
- ./data:/app/data
ports:
- '8123:8123'
environment:
- EXTERNAL_URL=http://localhost:8123
- EXTERNAL_URL=http://localhost:8123

89
go.mod
View File

@@ -1,35 +1,78 @@
module github.com/h44z/wg-portal
go 1.16
go 1.18
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-gonic/gin v1.7.4
github.com/go-ldap/ldap/v3 v3.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
git.prolicht.digital/golib/healthcheck v1.1.1
github.com/evanphx/json-patch v5.6.0+incompatible
github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.8.2
github.com/go-ldap/ldap/v3 v3.4.4
github.com/go-playground/validator/v10 v10.11.2
github.com/kelseyhightower/envconfig v1.4.0
github.com/mailru/easyjson v0.7.7 // indirect
github.com/milosgajdos/tenus v0.0.3
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.8.1
github.com/sirupsen/logrus v1.9.0
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/swaggo/files v1.0.0
github.com/swaggo/gin-swagger v1.5.3
github.com/swaggo/swag v1.8.10
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
github.com/xhit/go-simple-mail/v2 v2.10.0
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
gorm.io/driver/mysql v1.1.2
gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.21.13
github.com/xhit/go-simple-mail/v2 v2.13.0
golang.org/x/crypto v0.6.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.4.7
gorm.io/driver/sqlite v1.4.4
gorm.io/gorm v1.24.5
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/dchest/uniuri v1.2.0 // indirect
github.com/docker/libcontainer v2.2.1+incompatible // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.8 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-test/deep v1.0.8 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mdlayher/genetlink v1.3.1 // indirect
github.com/mdlayher/netlink v1.7.1 // indirect
github.com/mdlayher/socket v0.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.6.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20230216153314-c7b76d3d9ecd // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

368
go.sum
View File

@@ -1,96 +1,89 @@
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=
git.prolicht.digital/golib/healthcheck v1.1.1 h1:bdx0MuGqAq0PCooPpiuPXsr4/Ok+yfJwq8P9ITq2eLI=
git.prolicht.digital/golib/healthcheck v1.1.1/go.mod h1:wEqVrqHJ8NsSx5qlFGUlw74wJ/wDSKaA34QoyvsEkdc=
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/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/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
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/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
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/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
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/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
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/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
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/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
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-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
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/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU=
github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
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-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
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/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
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/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
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/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
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/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
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=
@@ -99,28 +92,19 @@ github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE
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/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/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/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
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=
@@ -132,190 +116,178 @@ 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/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mdlayher/genetlink v1.3.1 h1:roBiPnual+eqtRkKX2Jb8UQN5ZPWnhDCGj/wR6Jlz2w=
github.com/mdlayher/genetlink v1.3.1/go.mod h1:uaIPxkWmGk753VVIzDtROxQ8+T+dkHqOI0vB1NA9S/Q=
github.com/mdlayher/netlink v1.7.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTmg=
github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ=
github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc=
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/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
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/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/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/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc=
github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q=
github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI=
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo=
github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
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/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
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/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
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=
github.com/xhit/go-simple-mail/v2 v2.13.0 h1:OANWU9jHZrVfBkNkvLf8Ww0fexwpQVF/v/5f96fFTLI=
github.com/xhit/go-simple-mail/v2 v2.13.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
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/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/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-20210420072515-93ed5bcd2bfe/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
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/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
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/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
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=
golang.zx2c4.com/wireguard v0.0.0-20230216153314-c7b76d3d9ecd h1:thMXEWXMWIiGlp5T/V+CoetkzBJi4INNaglxdvyfK0c=
golang.zx2c4.com/wireguard v0.0.0-20230216153314-c7b76d3d9ecd/go.mod h1:whfbyDBt09xhCYQWtO2+3UVjlaq6/9hDZrjg2ZE6SyA=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde h1:ybF7AMzIUikL9x4LgwEmzhXtzRpKNqngme1VGDWz+Nk=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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=
@@ -324,23 +296,21 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
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=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE=
gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=

View File

@@ -2,6 +2,7 @@ package ldap
import (
"crypto/tls"
"os"
"strings"
"github.com/gin-gonic/gin"
@@ -47,8 +48,8 @@ func (Provider) GetPriority() int {
return 1 // LDAP password provider
}
func (provider Provider) SetupRoutes(routes *gin.RouterGroup) {
// nothing todo here
func (provider Provider) SetupRoutes(_ *gin.RouterGroup) {
// nothing here
}
func (provider Provider) Login(ctx *authentication.AuthContext) (string, error) {
@@ -96,8 +97,8 @@ func (provider Provider) Login(ctx *authentication.AuthContext) (string, error)
return sr.Entries[0].GetAttributeValue(provider.config.EmailAttribute), nil
}
func (provider Provider) Logout(context *authentication.AuthContext) error {
return nil // nothing todo here
func (provider Provider) Logout(_ *authentication.AuthContext) error {
return nil // nothing here
}
func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authentication.User, error) {
@@ -154,23 +155,49 @@ func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authent
}
func (provider Provider) open() (*ldap.Conn, error) {
tlsConfig := &tls.Config{InsecureSkipVerify: !provider.config.CertValidation}
var tlsConfig *tls.Config
if provider.config.LdapCertConn {
certPlain, err := os.ReadFile(provider.config.LdapTlsCert)
if err != nil {
return nil, errors.WithMessage(err, "failed to load the certificate")
}
key, err := os.ReadFile(provider.config.LdapTlsKey)
if err != nil {
return nil, errors.WithMessage(err, "failed to load the key")
}
certX509, err := tls.X509KeyPair(certPlain, key)
if err != nil {
return nil, errors.WithMessage(err, "failed X509")
}
tlsConfig = &tls.Config{Certificates: []tls.Certificate{certX509}}
} else {
tlsConfig = &tls.Config{InsecureSkipVerify: !provider.config.CertValidation}
}
conn, err := ldap.DialURL(provider.config.URL, ldap.DialWithTLSConfig(tlsConfig))
if err != nil {
return nil, err
return nil, errors.WithMessage(err, "failed to connect to LDAP")
}
if provider.config.StartTLS {
// Reconnect with TLS
err = conn.StartTLS(tlsConfig)
if err != nil {
return nil, err
return nil, errors.WithMessage(err, "failed to start TLS session")
}
}
err = conn.Bind(provider.config.BindUser, provider.config.BindPass)
if err != nil {
return nil, err
return nil, errors.WithMessage(err, "failed to bind user")
}
return conn, nil

View File

@@ -50,8 +50,8 @@ func (Provider) GetPriority() int {
return 0 // DB password provider = highest prio
}
func (provider Provider) SetupRoutes(routes *gin.RouterGroup) {
// nothing todo here
func (provider Provider) SetupRoutes(_ *gin.RouterGroup) {
// nothing here
}
func (provider Provider) Login(ctx *authentication.AuthContext) (string, error) {
@@ -79,8 +79,8 @@ func (provider Provider) Login(ctx *authentication.AuthContext) (string, error)
return user.Email, nil
}
func (provider Provider) Logout(context *authentication.AuthContext) error {
return nil // nothing todo here
func (provider Provider) Logout(_ *authentication.AuthContext) error {
return nil // nothing here
}
func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authentication.User, error) {

View File

@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/pkg/errors"
@@ -36,6 +37,40 @@ func init() {
return nil
},
})
migrations = append(migrations, Migration{
version: "1.0.9",
migrateFn: func(db *gorm.DB) error {
if db.Dialector.Name() != (sqlite.Dialector{}).Name() {
logrus.Infof("upgraded database format to version 1.0.9")
return nil // only perform migration for sqlite
}
type sqlIndex struct {
Name string `gorm:"column:name"`
Table string `gorm:"column:tbl_name"`
}
var indices []sqlIndex
if err := db.Raw("SELECT name, tbl_name FROM sqlite_master WHERE type == 'index'").Scan(&indices).Error; err != nil {
return errors.Wrap(err, "failed to fetch indices")
}
for _, index := range indices {
if index.Table != "devices" && index.Table != "peers" && index.Table != "users" {
continue
}
if strings.Contains(index.Name, "autoindex") {
continue
}
if err := db.Exec("DROP INDEX " + index.Name).Error; err != nil {
return errors.Wrap(err, "failed to drop index "+index.Name)
}
}
logrus.Infof("upgraded database format to version 1.0.9")
return nil
},
})
}
type SupportedDatabase string

View File

@@ -3,7 +3,6 @@ package common
import (
"crypto/tls"
"io"
"io/ioutil"
"time"
"github.com/pkg/errors"
@@ -96,7 +95,7 @@ func SendEmailWithAttachments(cfg MailConfig, sender, replyTo, subject, body, ht
email.AddAlternative(mail.TextPlain, body)
for _, attachment := range attachments {
attachmentData, err := ioutil.ReadAll(attachment.Data)
attachmentData, err := io.ReadAll(attachment.Data)
if err != nil {
return errors.Wrapf(err, "failed to read attachment data for %s", attachment.Name)
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"net"
"strings"
"time"
)
// BroadcastAddr returns the last address in the given network, or the broadcast address.
@@ -21,7 +22,7 @@ func BroadcastAddr(n *net.IPNet) net.IP {
return broadcast
}
// http://play.golang.org/p/m8TNTtygK0
// http://play.golang.org/p/m8TNTtygK0
func IncreaseIP(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
@@ -84,3 +85,11 @@ func ByteCountSI(b int64) string {
return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "kMGTPE"[exp])
}
func FormatDateHTML(t *time.Time) string {
if t == nil {
return ""
}
return t.Format("2006-01-02")
}

View File

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

View File

@@ -2,11 +2,19 @@ package ldap
import (
"crypto/tls"
"os"
"github.com/go-ldap/ldap/v3"
"github.com/pkg/errors"
)
type ObjectType int
const (
Users ObjectType = iota
Groups
)
type RawLdapData struct {
DN string
Attributes map[string]string
@@ -14,7 +22,33 @@ type RawLdapData struct {
}
func Open(cfg *Config) (*ldap.Conn, error) {
tlsConfig := &tls.Config{InsecureSkipVerify: !cfg.CertValidation}
var tlsConfig *tls.Config
if cfg.LdapCertConn {
certPlain, err := os.ReadFile(cfg.LdapTlsCert)
if err != nil {
return nil, errors.WithMessage(err, "failed to load the certificate")
}
key, err := os.ReadFile(cfg.LdapTlsKey)
if err != nil {
return nil, errors.WithMessage(err, "failed to load the key")
}
certX509, err := tls.X509KeyPair(certPlain, key)
if err != nil {
return nil, errors.WithMessage(err, "failed X509")
}
tlsConfig = &tls.Config{Certificates: []tls.Certificate{certX509}}
} else {
tlsConfig = &tls.Config{InsecureSkipVerify: !cfg.CertValidation}
}
conn, err := ldap.DialURL(cfg.URL, ldap.DialWithTLSConfig(tlsConfig))
if err != nil {
return nil, errors.Wrap(err, "failed to connect to LDAP")
@@ -42,21 +76,40 @@ func Close(conn *ldap.Conn) {
}
}
func FindAllUsers(cfg *Config) ([]RawLdapData, error) {
func FindAllObjects(cfg *Config, objType ObjectType) ([]RawLdapData, error) {
client, err := Open(cfg)
if err != nil {
return nil, errors.WithMessage(err, "failed to open ldap connection")
}
defer Close(client)
// Search all users
attrs := []string{"dn", cfg.EmailAttribute, cfg.EmailAttribute, cfg.FirstNameAttribute, cfg.LastNameAttribute,
cfg.PhoneAttribute, cfg.GroupMemberAttribute}
searchRequest := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
cfg.SyncFilter, attrs, nil,
)
var searchRequest *ldap.SearchRequest
var attrs []string
switch objType {
case Users:
// Search all users
attrs = []string{"dn", cfg.EmailAttribute, cfg.EmailAttribute, cfg.FirstNameAttribute, cfg.LastNameAttribute,
cfg.PhoneAttribute, cfg.GroupMemberAttribute}
searchRequest = ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
cfg.SyncFilter, attrs, nil,
)
case Groups:
if cfg.SyncGroupFilter == "" {
return nil, nil // no groups
}
// Search all groups
attrs = []string{"dn", cfg.GroupMemberAttribute}
searchRequest = ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
cfg.SyncGroupFilter, attrs, nil,
)
default:
panic("invalid object type")
}
sr, err := client.Search(searchRequest)
if err != nil {

View File

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

View File

@@ -12,6 +12,8 @@ import (
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
gldap "github.com/go-ldap/ldap/v3"
)
var ErrInvalidSpecification = errors.New("specification must be a struct pointer")
@@ -65,8 +67,12 @@ type Config struct {
EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"`
CreateDefaultPeer bool `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"`
SelfProvisioningAllowed bool `yaml:"selfProvisioning" envconfig:"SELF_PROVISIONING"`
WGExporterFriendlyNames bool `yaml:"wgExporterFriendlyNames" envconfig:"WG_EXPORTER_FRIENDLY_NAMES"`
LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"`
LogoUrl string `yaml:"logoUrl" envconfig:"LOGO_URL"`
BackgroundTaskInterval int `yaml:"backgroundTaskInterval" envconfig:"BACKGROUND_TASK_INTERVAL"` // in seconds
ExpiryReEnable bool `yaml:"expiryReEnable" envconfig:"EXPIRY_REENABLE"`
} `yaml:"core"`
Database common.DatabaseConfig `yaml:"database"`
Email common.MailConfig `yaml:"email"`
@@ -81,13 +87,16 @@ func NewConfig() *Config {
cfg.Core.ListeningAddress = ":8123"
cfg.Core.Title = "WireGuard VPN"
cfg.Core.CompanyName = "WireGuard Portal"
cfg.Core.LogoUrl = "/img/header-logo.png"
cfg.Core.ExternalUrl = "http://localhost:8123"
cfg.Core.MailFrom = "WireGuard VPN <noreply@company.com>"
cfg.Core.AdminUser = "admin@wgportal.local"
cfg.Core.AdminPassword = "wgportal"
cfg.Core.LdapEnabled = false
cfg.Core.EditableKeys = true
cfg.Core.WGExporterFriendlyNames = false
cfg.Core.SessionSecret = "secret"
cfg.Core.BackgroundTaskInterval = 15 * 60 // 15 minutes
cfg.Database.Typ = "sqlite"
cfg.Database.Database = "data/wg_portal.db"
@@ -105,11 +114,13 @@ func NewConfig() *Config {
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.LDAP.SyncGroupFilter = ""
cfg.WG.DeviceNames = []string{"wg0"}
cfg.WG.DefaultDeviceName = "wg0"
cfg.WG.ConfigDirectoryPath = "/etc/wireguard"
cfg.WG.ManageIPAddresses = true
cfg.WG.UserManagePeers = false
cfg.Email.Host = "127.0.0.1"
cfg.Email.Port = 25
cfg.Email.Encryption = common.MailEncryptionNone
@@ -128,6 +139,10 @@ func NewConfig() *Config {
if err != nil {
logrus.Warnf("unable to load environment config: %v", err)
}
cfg.LDAP.AdminLdapGroup_, err = gldap.ParseDN(cfg.LDAP.AdminLdapGroup)
if err != nil {
logrus.Warnf("Parsing AdminLDAPGroup failed: %v", err)
}
if cfg.WG.ManageIPAddresses && runtime.GOOS != "linux" {
logrus.Warnf("managing IP addresses only works on linux, feature disabled...")

View File

@@ -1,22 +1,14 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
package docs
import (
"bytes"
"encoding/json"
"strings"
import "github.com/swaggo/swag"
"github.com/alecthomas/template"
"github.com/swaggo/swag"
)
var doc = `{
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{.Description}}",
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {
"name": "WireGuard Portal Project",
@@ -45,11 +37,12 @@ var doc = `{
"Interface"
],
"summary": "Get the given device",
"operationId": "GetDevice",
"parameters": [
{
"type": "string",
"description": "Device Name",
"name": "device",
"name": "DeviceName",
"in": "query",
"required": true
}
@@ -103,17 +96,18 @@ var doc = `{
"Interface"
],
"summary": "Updates the given device based on the given device model (UNIMPLEMENTED)",
"operationId": "PutDevice",
"parameters": [
{
"type": "string",
"description": "Device Name",
"name": "device",
"name": "DeviceName",
"in": "query",
"required": true
},
{
"description": "Device Model",
"name": "body",
"name": "Device",
"in": "body",
"required": true,
"schema": {
@@ -176,17 +170,18 @@ var doc = `{
"Interface"
],
"summary": "Updates the given device based on the given partial device model (UNIMPLEMENTED)",
"operationId": "PatchDevice",
"parameters": [
{
"type": "string",
"description": "Device Name",
"name": "device",
"name": "DeviceName",
"in": "query",
"required": true
},
{
"description": "Device Model",
"name": "body",
"name": "Device",
"in": "body",
"required": true,
"schema": {
@@ -248,6 +243,7 @@ var doc = `{
"Interface"
],
"summary": "Get all devices",
"operationId": "GetDevices",
"responses": {
"200": {
"description": "OK",
@@ -299,11 +295,12 @@ var doc = `{
"Peers"
],
"summary": "Retrieves the peer for the given public key",
"operationId": "GetPeer",
"parameters": [
{
"type": "string",
"description": "Public Key (Base 64)",
"name": "pkey",
"name": "PublicKey",
"in": "query",
"required": true
}
@@ -351,17 +348,18 @@ var doc = `{
"Peers"
],
"summary": "Updates the given peer based on the given peer model",
"operationId": "PutPeer",
"parameters": [
{
"type": "string",
"description": "Public Key",
"name": "pkey",
"name": "PublicKey",
"in": "query",
"required": true
},
{
"description": "Peer Model",
"name": "peer",
"name": "Peer",
"in": "body",
"required": true,
"schema": {
@@ -421,17 +419,18 @@ var doc = `{
"Peers"
],
"summary": "Updates the given peer based on the given partial peer model",
"operationId": "DeletePeer",
"parameters": [
{
"type": "string",
"description": "Public Key",
"name": "pkey",
"name": "PublicKey",
"in": "query",
"required": true
}
],
"responses": {
"202": {
"204": {
"description": "No Content"
},
"400": {
@@ -482,17 +481,18 @@ var doc = `{
"Peers"
],
"summary": "Updates the given peer based on the given partial peer model",
"operationId": "PatchPeer",
"parameters": [
{
"type": "string",
"description": "Public Key",
"name": "pkey",
"name": "PublicKey",
"in": "query",
"required": true
},
{
"description": "Peer Model",
"name": "peer",
"name": "Peer",
"in": "body",
"required": true,
"schema": {
@@ -554,11 +554,12 @@ var doc = `{
"Peers"
],
"summary": "Retrieves all peers for the given interface",
"operationId": "GetPeers",
"parameters": [
{
"type": "string",
"description": "Device Name",
"name": "device",
"name": "DeviceName",
"in": "query",
"required": true
}
@@ -609,17 +610,18 @@ var doc = `{
"Peers"
],
"summary": "Creates a new peer based on the given peer model",
"operationId": "PostPeer",
"parameters": [
{
"type": "string",
"description": "Device Name",
"name": "device",
"name": "DeviceName",
"in": "query",
"required": true
},
{
"description": "Peer Model",
"name": "peer",
"name": "Peer",
"in": "body",
"required": true,
"schema": {
@@ -681,11 +683,12 @@ var doc = `{
"Users"
],
"summary": "Retrieves user based on given Email",
"operationId": "GetUser",
"parameters": [
{
"type": "string",
"description": "User Email",
"name": "email",
"name": "Email",
"in": "query",
"required": true
}
@@ -739,17 +742,18 @@ var doc = `{
"Users"
],
"summary": "Updates a user based on the given user model",
"operationId": "PutUser",
"parameters": [
{
"type": "string",
"description": "User Email",
"name": "email",
"name": "Email",
"in": "query",
"required": true
},
{
"description": "User Model",
"name": "user",
"name": "User",
"in": "body",
"required": true,
"schema": {
@@ -809,11 +813,12 @@ var doc = `{
"Users"
],
"summary": "Deletes the specified user",
"operationId": "DeleteUser",
"parameters": [
{
"type": "string",
"description": "User Email",
"name": "email",
"name": "Email",
"in": "query",
"required": true
}
@@ -870,17 +875,18 @@ var doc = `{
"Users"
],
"summary": "Updates a user based on the given partial user model",
"operationId": "PatchUser",
"parameters": [
{
"type": "string",
"description": "User Email",
"name": "email",
"name": "Email",
"in": "query",
"required": true
},
{
"description": "User Model",
"name": "user",
"name": "User",
"in": "body",
"required": true,
"schema": {
@@ -942,6 +948,7 @@ var doc = `{
"Users"
],
"summary": "Retrieves all users",
"operationId": "GetUsers",
"responses": {
"200": {
"description": "OK",
@@ -988,10 +995,11 @@ var doc = `{
"Users"
],
"summary": "Creates a new user based on the given user model",
"operationId": "PostUser",
"parameters": [
{
"description": "User Model",
"name": "user",
"name": "User",
"in": "body",
"required": true,
"schema": {
@@ -1053,11 +1061,12 @@ var doc = `{
"Provisioning"
],
"summary": "Retrieves the peer config for the given public key",
"operationId": "GetPeerDeploymentConfig",
"parameters": [
{
"type": "string",
"description": "Public Key (Base 64)",
"name": "pkey",
"name": "PublicKey",
"in": "query",
"required": true
}
@@ -1104,11 +1113,12 @@ var doc = `{
"Provisioning"
],
"summary": "Retrieves all active peers for the given email address",
"operationId": "GetPeerDeploymentInformation",
"parameters": [
{
"type": "string",
"description": "Email Address",
"name": "email",
"name": "Email",
"in": "query",
"required": true
}
@@ -1159,10 +1169,11 @@ var doc = `{
"Provisioning"
],
"summary": "Creates the requested peer config and returns the config file",
"operationId": "PostPeerDeploymentConfig",
"parameters": [
{
"description": "Provisioning Request Model",
"name": "body",
"name": "ProvisioningRequest",
"in": "body",
"required": true,
"schema": {
@@ -1200,22 +1211,10 @@ var doc = `{
}
},
"definitions": {
"gorm.DeletedAt": {
"type": "object",
"properties": {
"time": {
"type": "string"
},
"valid": {
"description": "Valid is true if Time is not NULL",
"type": "boolean"
}
}
},
"server.ApiError": {
"type": "object",
"properties": {
"message": {
"Message": {
"type": "string"
}
}
@@ -1223,16 +1222,16 @@ var doc = `{
"server.PeerDeploymentInformation": {
"type": "object",
"properties": {
"device": {
"Device": {
"type": "string"
},
"deviceIdentifier": {
"DeviceIdentifier": {
"type": "string"
},
"identifier": {
"Identifier": {
"type": "string"
},
"publicKey": {
"PublicKey": {
"type": "string"
}
}
@@ -1240,74 +1239,77 @@ var doc = `{
"server.ProvisioningRequest": {
"type": "object",
"required": [
"email",
"identifier"
"Email",
"Identifier"
],
"properties": {
"allowedIPsStr": {
"AllowedIPsStr": {
"type": "string"
},
"deviceName": {
"DNSStr": {
"type": "string"
},
"DeviceName": {
"description": "DeviceName is optional, if not specified, the configured default device will be used.",
"type": "string"
},
"dnsstr": {
"Email": {
"type": "string"
},
"email": {
"Identifier": {
"type": "string"
},
"identifier": {
"type": "string"
"Mtu": {
"type": "integer",
"maximum": 1500,
"minimum": 0
},
"mtu": {
"type": "integer"
},
"persistentKeepalive": {
"type": "integer"
"PersistentKeepalive": {
"type": "integer",
"minimum": 0
}
}
},
"users.User": {
"type": "object",
"required": [
"email",
"firstname",
"lastname"
"Email",
"Firstname",
"Lastname"
],
"properties": {
"createdAt": {
"CreatedAt": {
"description": "database internal fields",
"type": "string"
},
"deletedAt": {
"$ref": "#/definitions/gorm.DeletedAt"
"DeletedAt": {
"type": "string"
},
"email": {
"Email": {
"description": "required fields",
"type": "string"
},
"firstname": {
"Firstname": {
"description": "optional fields",
"type": "string"
},
"isAdmin": {
"IsAdmin": {
"type": "boolean"
},
"lastname": {
"Lastname": {
"type": "string"
},
"password": {
"Password": {
"description": "optional, integrated password authentication",
"type": "string"
},
"phone": {
"Phone": {
"type": "string"
},
"source": {
"Source": {
"type": "string"
},
"updatedAt": {
"UpdatedAt": {
"type": "string"
}
}
@@ -1315,87 +1317,96 @@ var doc = `{
"wireguard.Device": {
"type": "object",
"required": [
"deviceName",
"ipsStr",
"privateKey",
"publicKey",
"type"
"DeviceName",
"IPsStr",
"PrivateKey",
"PublicKey",
"Type"
],
"properties": {
"createdAt": {
"CreatedAt": {
"type": "string"
},
"defaultAllowedIPsStr": {
"description": "comma separated list of IPs that are used in the client config file",
"type": "string"
},
"defaultEndpoint": {
"description": "Settings that are applied to all peer by default",
"type": "string"
},
"defaultPersistentKeepalive": {
"type": "integer"
},
"deviceName": {
"type": "string"
},
"displayName": {
"type": "string"
},
"dnsstr": {
"DNSStr": {
"description": "comma separated list of the DNS servers of the client, wg-quick addition",
"type": "string"
},
"firewallMark": {
"type": "integer"
"DefaultAllowedIPsStr": {
"description": "comma separated list of IPs that are used in the client config file",
"type": "string"
},
"ipsStr": {
"DefaultEndpoint": {
"description": "Settings that are applied to all peer by default",
"type": "string"
},
"DefaultPersistentKeepalive": {
"type": "integer",
"minimum": 0
},
"DeviceName": {
"type": "string"
},
"DisplayName": {
"type": "string",
"maxLength": 200
},
"FirewallMark": {
"type": "integer",
"minimum": 0
},
"IPsStr": {
"description": "comma separated list of the IPs of the client, wg-quick addition",
"type": "string"
},
"listenPort": {
"ListenPort": {
"type": "integer"
},
"mtu": {
"Mtu": {
"description": "the interface MTU, wg-quick addition",
"type": "integer"
"type": "integer",
"maximum": 1500,
"minimum": 0
},
"postDown": {
"PostDown": {
"description": "post down script, wg-quick addition",
"type": "string"
},
"postUp": {
"PostUp": {
"description": "post up script, wg-quick addition",
"type": "string"
},
"preDown": {
"PreDown": {
"description": "pre down script, wg-quick addition",
"type": "string"
},
"preUp": {
"PreUp": {
"description": "pre up script, wg-quick addition",
"type": "string"
},
"privateKey": {
"PrivateKey": {
"description": "Core WireGuard Settings (Interface section)",
"type": "string"
},
"publicKey": {
"PublicKey": {
"description": "Misc. WireGuard Settings",
"type": "string"
},
"routingTable": {
"RoutingTable": {
"description": "the routing table, wg-quick addition",
"type": "string"
},
"saveConfig": {
"SaveConfig": {
"description": "if set to ` + "`" + `true', the configuration is saved from the current state of the interface upon shutdown, wg-quick addition",
"type": "boolean"
},
"type": {
"type": "string"
"Type": {
"type": "string",
"enum": [
"client",
"server"
]
},
"updatedAt": {
"UpdatedAt": {
"type": "string"
}
}
@@ -1403,71 +1414,98 @@ var doc = `{
"wireguard.Peer": {
"type": "object",
"required": [
"deviceName",
"email",
"identifier",
"publicKey"
"DeviceName",
"DeviceType",
"Email",
"Identifier",
"PublicKey",
"UID"
],
"properties": {
"allowedIPsStr": {
"AllowedIPsSrvStr": {
"description": "a comma separated list of IPs that are used in the server config file",
"type": "string"
},
"AllowedIPsStr": {
"description": "a comma separated list of IPs that are used in the client config file",
"type": "string"
},
"createdAt": {
"CreatedAt": {
"type": "string"
},
"createdBy": {
"CreatedBy": {
"type": "string"
},
"deactivatedAt": {
"type": "string"
},
"deviceName": {
"type": "string"
},
"dnsstr": {
"DNSStr": {
"description": "comma separated list of the DNS servers for the client",
"type": "string"
},
"email": {
"DeactivatedAt": {
"type": "string"
},
"endpoint": {
"DeactivatedReason": {
"type": "string"
},
"identifier": {
"description": "Identifier AND Email make a WireGuard peer unique",
"DeviceName": {
"type": "string"
},
"ignoreGlobalSettings": {
"type": "boolean"
"DeviceType": {
"type": "string",
"enum": [
"client",
"server"
]
},
"ipsStr": {
"Email": {
"type": "string"
},
"Endpoint": {
"type": "string"
},
"ExpiresAt": {
"type": "string"
},
"IPsStr": {
"description": "a comma separated list of IPs of the client",
"type": "string"
},
"mtu": {
"Identifier": {
"description": "Identifier AND Email make a WireGuard peer unique",
"type": "string",
"maxLength": 64
},
"IgnoreGlobalSettings": {
"type": "boolean"
},
"Mtu": {
"description": "Global Device Settings (can be ignored, only make sense if device is in server mode)",
"type": "integer"
"type": "integer",
"maximum": 1500,
"minimum": 0
},
"persistentKeepalive": {
"type": "integer"
"PersistentKeepalive": {
"type": "integer",
"minimum": 0
},
"presharedKey": {
"PresharedKey": {
"type": "string"
},
"privateKey": {
"PrivateKey": {
"description": "Misc. WireGuard Settings",
"type": "string"
},
"publicKey": {
"PublicKey": {
"description": "Core WireGuard Settings",
"type": "string"
},
"updatedAt": {
"UID": {
"description": "uid for html identification",
"type": "string"
},
"updatedBy": {
"UpdatedAt": {
"type": "string"
},
"UpdatedBy": {
"type": "string"
}
}
@@ -1483,49 +1521,18 @@ var doc = `{
}
}`
type swaggerInfo struct {
Version string
Host string
BasePath string
Schemes []string
Title string
Description string
}
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = swaggerInfo{
Version: "1.0",
Host: "",
BasePath: "/api/v1",
Schemes: []string{},
Title: "WireGuard Portal API",
Description: "WireGuard Portal API for managing users and peers.",
}
type s struct{}
func (s *s) ReadDoc() string {
sInfo := SwaggerInfo
sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
t, err := template.New("swagger_info").Funcs(template.FuncMap{
"marshal": func(v interface{}) string {
a, _ := json.Marshal(v)
return string(a)
},
}).Parse(doc)
if err != nil {
return doc
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, sInfo); err != nil {
return doc
}
return tpl.String()
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "",
BasePath: "/api/v1",
Schemes: []string{},
Title: "WireGuard Portal API",
Description: "WireGuard Portal API for managing users and peers.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
}
func init() {
swag.Register(swag.Name, &s{})
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

View File

@@ -135,15 +135,16 @@ func (s *Server) GetUserIndex(c *gin.Context) {
peers := s.peers.GetSortedPeersForEmail(currentSession.SortedBy["userpeers"], currentSession.SortDirection["userpeers"], currentSession.Email)
c.HTML(http.StatusOK, "user_index.html", gin.H{
"Route": c.Request.URL.Path,
"Alerts": GetFlashes(c),
"Session": currentSession,
"Static": s.getStaticData(),
"Peers": peers,
"TotalPeers": len(peers),
"Users": []users.User{*s.users.GetUser(currentSession.Email)},
"Device": s.peers.GetDevice(currentSession.DeviceName),
"DeviceNames": s.GetDeviceNames(),
"Route": c.Request.URL.Path,
"Alerts": GetFlashes(c),
"Session": currentSession,
"Static": s.getStaticData(),
"Peers": peers,
"TotalPeers": len(peers),
"Users": []users.User{*s.users.GetUser(currentSession.Email)},
"Device": s.peers.GetDevice(currentSession.DeviceName),
"DeviceNames": s.GetDeviceNames(),
"UserManagePeers": s.config.WG.UserManagePeers,
})
}

View File

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

View File

@@ -71,8 +71,15 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) {
now := time.Now()
if disabled && currentPeer.DeactivatedAt == nil {
formPeer.DeactivatedAt = &now
formPeer.DeactivatedReason = wireguard.DeactivatedReasonAdminEdit
} else if !disabled {
formPeer.DeactivatedAt = nil
formPeer.DeactivatedReason = ""
// If a peer was deactivated due to expiry, remove the expires-at date to avoid
// unwanted re-expiry.
if currentPeer.DeactivatedReason == wireguard.DeactivatedReasonExpired {
formPeer.ExpiresAt = nil
}
}
// Update in database
@@ -129,6 +136,7 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) {
now := time.Now()
if disabled {
formPeer.DeactivatedAt = &now
formPeer.DeactivatedReason = wireguard.DeactivatedReasonAdminCreate
}
if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil {
@@ -189,7 +197,7 @@ func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) {
logrus.Infof("creating %d ldap peers", len(emails))
for i := range emails {
if err := s.CreatePeerByEmail(currentSession.DeviceName, emails[i], formData.Identifier, false); err != nil {
if err := s.CreatePeerByEmail(currentSession.DeviceName, emails[i], formData.Identifier); err != nil {
_ = s.updateFormInSession(c, formData)
SetFlashMessage(c, "failed to add user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=create")
@@ -236,7 +244,7 @@ func (s *Server) GetPeerConfig(c *gin.Context) {
return
}
cfg, err := peer.GetConfigFile(s.peers.GetDevice(currentSession.DeviceName))
cfg, err := peer.GetConfigFile(s.peers.GetDevice(peer.DeviceName))
if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return
@@ -392,3 +400,122 @@ func (s *Server) sendPeerConfigMail(peer wireguard.Peer) error {
return nil
}
func (s *Server) GetUserCreatePeer(c *gin.Context) {
currentSession, err := s.setNewPeerFormInSession(c)
if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
return
}
c.HTML(http.StatusOK, "user_create_client.html", gin.H{
"Route": c.Request.URL.Path,
"Alerts": GetFlashes(c),
"Session": currentSession,
"Static": s.getStaticData(),
"Peer": currentSession.FormData.(wireguard.Peer),
"EditableKeys": s.config.Core.EditableKeys,
"Device": s.peers.GetDevice(currentSession.DeviceName),
"DeviceNames": s.GetDeviceNames(),
"AdminEmail": s.config.Core.AdminUser,
"Csrf": csrf.GetToken(c),
})
}
func (s *Server) PostUserCreatePeer(c *gin.Context) {
currentSession := GetSessionData(c)
var formPeer wireguard.Peer
if currentSession.FormData != nil {
formPeer = currentSession.FormData.(wireguard.Peer)
}
formPeer.Email = currentSession.Email
formPeer.Identifier = currentSession.Email
formPeer.DeviceType = wireguard.DeviceTypeServer
if err := c.ShouldBind(&formPeer); err != nil {
_ = s.updateFormInSession(c, formPeer)
SetFlashMessage(c, "failed to bind form data: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/user/peer/create?formerr=bind")
return
}
// if public key was manually set, remove the incorrect private key
if formPeer.PublicKey != currentSession.FormData.(wireguard.Peer).PublicKey {
formPeer.PrivateKey = ""
}
disabled := c.PostForm("isdisabled") != ""
now := time.Now()
if disabled {
formPeer.DeactivatedAt = &now
formPeer.DeactivatedReason = wireguard.DeactivatedReasonUserCreate
}
if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil {
_ = s.updateFormInSession(c, formPeer)
SetFlashMessage(c, "failed to add user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/user/peer/create?formerr=create")
return
}
SetFlashMessage(c, "client created successfully", "success")
c.Redirect(http.StatusSeeOther, "/user/profile")
}
func (s *Server) GetUserEditPeer(c *gin.Context) {
peer := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession, err := s.setFormInSession(c, peer)
if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
return
}
if peer.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return
}
c.HTML(http.StatusOK, "user_edit_client.html", gin.H{
"Route": c.Request.URL.Path,
"Alerts": GetFlashes(c),
"Session": currentSession,
"Static": s.getStaticData(),
"Peer": currentSession.FormData.(wireguard.Peer),
"EditableKeys": s.config.Core.EditableKeys,
"Device": s.peers.GetDevice(currentSession.DeviceName),
"DeviceNames": s.GetDeviceNames(),
"AdminEmail": s.config.Core.AdminUser,
"Csrf": csrf.GetToken(c),
})
}
func (s *Server) PostUserEditPeer(c *gin.Context) {
currentPeer := s.peers.GetPeerByKey(c.Query("pkey"))
urlEncodedKey := url.QueryEscape(c.Query("pkey"))
currentSession := GetSessionData(c)
if currentPeer.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return
}
disabled := c.PostForm("isdisabled") != ""
now := time.Now()
if disabled && currentPeer.DeactivatedAt == nil {
currentPeer.DeactivatedAt = &now
currentPeer.DeactivatedReason = wireguard.DeactivatedReasonUserEdit
}
// Update in database
if err := s.UpdatePeer(currentPeer, now); err != nil {
_ = s.updateFormInSession(c, currentPeer)
SetFlashMessage(c, "failed to update user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/user/peer/edit?pkey="+urlEncodedKey+"&formerr=update")
return
}
SetFlashMessage(c, "changes applied successfully", "success")
c.Redirect(http.StatusSeeOther, "/user/peer/edit?pkey="+urlEncodedKey)
}

View File

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

View File

@@ -4,16 +4,42 @@ import (
"strings"
"time"
gldap "github.com/go-ldap/ldap/v3"
"github.com/h44z/wg-portal/internal/ldap"
"github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
func (s *Server) SyncLdapWithUserDatabase() {
logrus.Info("starting ldap user synchronization...")
running := true
for running {
// Main work here
logrus.Trace("syncing ldap users to database...")
ldapUsers, err := ldap.FindAllObjects(&s.config.LDAP, ldap.Users)
if err != nil {
logrus.Errorf("failed to fetch users from ldap: %v", err)
continue
}
ldapGroups, err := ldap.FindAllObjects(&s.config.LDAP, ldap.Groups)
if err != nil {
logrus.Errorf("failed to fetch groups from ldap: %v", err)
continue
}
logrus.Tracef("found %d users and %d groups in ldap", len(ldapUsers), len(ldapGroups))
// Update existing LDAP users
s.updateLdapUsers(ldapUsers, ldapGroups)
// Disable missing LDAP users
s.disableMissingLdapUsers(ldapUsers)
logrus.Trace("synchronized ldap users to database")
// Select blocks until one of the cases happens
select {
case <-time.After(1 * time.Minute):
@@ -23,26 +49,71 @@ func (s *Server) SyncLdapWithUserDatabase() {
running = false
continue
}
// Main work here
logrus.Trace("syncing ldap users to database...")
ldapUsers, err := ldap.FindAllUsers(&s.config.LDAP)
if err != nil {
logrus.Errorf("failed to fetch users from ldap: %v", err)
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 {
func (s Server) userIsInAdminGroup(userData *ldap.RawLdapData, groupTreeData []ldap.RawLdapData) bool {
if s.config.LDAP.EveryoneAdmin {
return true
}
if s.config.LDAP.AdminLdapGroup_ == nil {
return false
}
for _, userGroup := range userData.RawAttributes[s.config.LDAP.GroupMemberAttribute] {
var userGroupDn, _ = gldap.ParseDN(string(userGroup))
if s.dnIsAdminGroup(userGroupDn, groupTreeData) {
return true
}
}
return false
}
// dnIsAdminGroup checks if the given DN is equal to the admin group, or if it is included in a groupTree that has the
// admin group as parent/root.
//
// WGPortal-Admin (L0)
//
// \_ IT-Admin (L1)
// |_ Alice (L2)
// |_ Bob (L2)
// \_ Eve (L2)
// \_ External-Company (L1)
// |_ External-Admin (L2)
// |_ Sam (L3)
// \_ Steve (L3)
//
// All DNs in the example above are member of the admin group.
func (s Server) dnIsAdminGroup(dn *gldap.DN, groupTreeData []ldap.RawLdapData) bool {
if s.config.LDAP.AdminLdapGroup_ == nil {
return false
}
if s.config.LDAP.AdminLdapGroup_.Equal(dn) {
return true
}
// Recursively check the whole group tree
for _, group := range groupTreeData {
var groupDn, _ = gldap.ParseDN(group.DN)
if !dn.Equal(groupDn) {
continue
}
for _, parentGroupDn := range group.RawAttributes[s.config.LDAP.GroupMemberAttribute] {
var parentDn, _ = gldap.ParseDN(string(parentGroupDn))
if s.dnIsAdminGroup(parentDn, groupTreeData) {
return true
}
}
break
}
return false
}
func (s Server) userChangedInLdap(user *users.User, ldapData *ldap.RawLdapData, ldapGroupData []ldap.RawLdapData) bool {
if user.Firstname != ldapData.Attributes[s.config.LDAP.FirstNameAttribute] {
return true
}
@@ -63,14 +134,7 @@ func (s Server) userChangedInLdap(user *users.User, ldapData *ldap.RawLdapData)
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 {
if user.IsAdmin != s.userIsInAdminGroup(ldapData, ldapGroupData) {
return true
}
@@ -101,18 +165,19 @@ func (s *Server) disableMissingLdapUsers(ldapUsers []ldap.RawLdapData) {
for _, peer := range s.peers.GetPeersByMail(activeUsers[i].Email) {
now := time.Now()
peer.DeactivatedAt = &now
peer.DeactivatedReason = wireguard.DeactivatedReasonLdapMissing
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 {
if err := s.users.DeleteUser(&activeUsers[i], true); err != nil {
logrus.Errorf("failed to delete deactivated user %s in database: %v", activeUsers[i].Email, err)
}
}
}
func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData) {
func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData, ldapGroups []ldap.RawLdapData) {
for i := range ldapUsers {
if ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute] == "" {
logrus.Tracef("skipping sync of %s, empty email attribute", ldapUsers[i].DN)
@@ -130,6 +195,7 @@ func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData) {
for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now()
peer.DeactivatedAt = nil
peer.DeactivatedReason = ""
if err = s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update activated peer %s: %v", peer.PublicKey, err)
}
@@ -137,23 +203,16 @@ func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData) {
}
// Sync attributes from ldap
if s.userChangedInLdap(user, &ldapUsers[i]) {
if s.userChangedInLdap(user, &ldapUsers[i], ldapGroups) {
logrus.Debugf("updating ldap user %s", user.Email)
user.Firstname = ldapUsers[i].Attributes[s.config.LDAP.FirstNameAttribute]
user.Lastname = ldapUsers[i].Attributes[s.config.LDAP.LastNameAttribute]
user.Email = ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute]
user.Phone = ldapUsers[i].Attributes[s.config.LDAP.PhoneAttribute]
user.IsAdmin = false
user.IsAdmin = s.userIsInAdminGroup(&ldapUsers[i], ldapGroups)
user.Source = users.UserSourceLdap
user.DeletedAt = gorm.DeletedAt{} // Not deleted
for _, group := range ldapUsers[i].RawAttributes[s.config.LDAP.GroupMemberAttribute] {
if string(group) == s.config.LDAP.AdminLdapGroup {
user.IsAdmin = true
break
}
}
if err = s.users.UpdateUser(user); err != nil {
logrus.Errorf("failed to update ldap user %s in database: %v", user.Email, err)
continue

View File

@@ -7,8 +7,8 @@ import (
"github.com/gin-gonic/gin"
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.
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
csrf "github.com/utrack/gin-csrf"
)
@@ -64,6 +64,7 @@ func SetupRoutes(s *Server) {
admin.GET("/users/create", s.GetAdminUsersCreate)
admin.POST("/users/create", s.PostAdminUsersCreate)
admin.GET("/users/edit", s.GetAdminUsersEdit)
admin.GET("/users/delete", s.GetAdminUsersDelete)
admin.POST("/users/edit", s.PostAdminUsersEdit)
// User routes
@@ -75,6 +76,13 @@ func SetupRoutes(s *Server) {
user.GET("/download", s.GetPeerConfig)
user.GET("/email", s.GetPeerConfigMail)
user.GET("/status", s.GetPeerStatus)
if s.config.WG.UserManagePeers {
user.GET("/peer/create", s.GetUserCreatePeer)
user.POST("/peer/create", s.PostUserCreatePeer)
user.GET("/peer/edit", s.GetUserEditPeer)
user.POST("/peer/edit", s.PostUserEditPeer)
}
}
func SetupApiRoutes(s *Server) {

View File

@@ -4,8 +4,8 @@ import (
"context"
"encoding/gob"
"html/template"
"io"
"io/fs"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
@@ -110,7 +110,7 @@ func (s *Server) Setup(ctx context.Context) error {
// Setup http server
gin.SetMode(gin.DebugMode)
gin.DefaultWriter = ioutil.Discard
gin.DefaultWriter = io.Discard
s.server = gin.New()
if logrus.GetLevel() == logrus.TraceLevel {
s.server.Use(ginlogrus.Logger(logrus.StandardLogger()))
@@ -127,6 +127,7 @@ func (s *Server) Setup(ctx context.Context) error {
})
s.server.Use(sessions.Sessions("authsession", cookieStore))
s.server.SetFuncMap(template.FuncMap{
"formatDate": common.FormatDateHTML,
"formatBytes": common.ByteCountSI,
"urlEncode": url.QueryEscape,
"startsWith": strings.HasPrefix,
@@ -215,6 +216,8 @@ func (s *Server) Run() {
go s.SyncLdapWithUserDatabase()
}
go s.RunBackgroundTasks(s.ctx)
// Run web service
srv := &http.Server{
Addr: s.config.Core.ListeningAddress,
@@ -253,7 +256,7 @@ func (s *Server) getExecutableDirectory() string {
func (s *Server) getStaticData() StaticData {
return StaticData{
WebsiteTitle: s.config.Core.Title,
WebsiteLogo: "/img/header-logo.png",
WebsiteLogo: s.config.Core.LogoUrl,
CompanyName: s.config.Core.CompanyName,
Year: time.Now().Year(),
Version: Version,

View File

@@ -1,9 +1,10 @@
package server
import (
"context"
"crypto/md5"
"fmt"
"io/ioutil"
"os"
"path"
"syscall"
"time"
@@ -62,7 +63,7 @@ func (s *Server) PrepareNewPeer(device string) (wireguard.Peer, error) {
}
// CreatePeerByEmail creates a new peer for the given email.
func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string, disabled bool) error {
func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string) error {
user := s.users.GetUser(email)
peer, err := s.PrepareNewPeer(device)
@@ -75,10 +76,6 @@ func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string, disab
} else {
peer.Identifier = fmt.Sprintf("%s (%s)", email, identifierSuffix)
}
now := time.Now()
if disabled {
peer.DeactivatedAt = &now
}
return s.CreatePeer(device, peer)
}
@@ -103,21 +100,29 @@ func (s *Server) CreatePeer(device string, peer wireguard.Peer) error {
}
peer.SetIPAddresses(peerIPs...)
}
if peer.PrivateKey == "" && dev.Type == wireguard.DeviceTypeServer { // if private key is empty create a new one
if peer.PresharedKey == "" && dev.Type == wireguard.DeviceTypeServer { // if preshared key is empty create a new one
psk, err := wgtypes.GenerateKey()
if err != nil {
return errors.Wrap(err, "failed to generate key")
}
peer.PresharedKey = psk.String()
}
if peer.PrivateKey == "" && peer.PublicKey == "" && dev.Type == wireguard.DeviceTypeServer { // if private key is empty create a new one
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return errors.Wrap(err, "failed to generate private key")
}
peer.PresharedKey = psk.String()
peer.PrivateKey = key.String()
peer.PublicKey = key.PublicKey().String()
}
peer.DeviceName = dev.DeviceName
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
if peer.ExpiresAt != nil && peer.ExpiresAt.IsZero() { // convert 01-01-0001 to nil
peer.ExpiresAt = nil
}
// Create WireGuard interface
if peer.DeactivatedAt == nil {
@@ -139,6 +144,13 @@ func (s *Server) UpdatePeer(peer wireguard.Peer, updateTime time.Time) error {
currentPeer := s.peers.GetPeerByKey(peer.PublicKey)
dev := s.peers.GetDevice(peer.DeviceName)
// Check if expiry date is in the future, an reactivate the peer in case.
if s.config.Core.ExpiryReEnable && currentPeer.DeactivatedReason == wireguard.DeactivatedReasonExpired &&
peer.ExpiresAt != nil && peer.ExpiresAt.After(time.Now()) {
peer.DeactivatedAt = nil
peer.DeactivatedReason = ""
}
// Update WireGuard device
var err error
switch {
@@ -154,6 +166,9 @@ func (s *Server) UpdatePeer(peer wireguard.Peer, updateTime time.Time) error {
}
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
if peer.ExpiresAt != nil && peer.ExpiresAt.IsZero() { // convert 01-01-0001 to nil
peer.ExpiresAt = nil
}
// Update in database
if err := s.peers.UpdatePeer(peer); err != nil {
@@ -204,12 +219,12 @@ func (s *Server) WriteWireGuardConfigFile(device string) error {
}
dev := s.peers.GetDevice(device)
cfg, err := dev.GetConfigFile(s.peers.GetActivePeers(device))
cfg, err := dev.GetConfigFile(s.peers.GetActivePeers(device), s.config.Core.WGExporterFriendlyNames)
if err != nil {
return errors.WithMessage(err, "failed to get config file")
}
filePath := path.Join(s.config.WG.ConfigDirectoryPath, dev.DeviceName+".conf")
if err := ioutil.WriteFile(filePath, cfg, 0644); err != nil {
if err := os.WriteFile(filePath, cfg, 0644); err != nil {
return errors.Wrap(err, "failed to write WireGuard config file")
}
return nil
@@ -248,10 +263,6 @@ func (s *Server) CreateUser(user users.User, device string) error {
// UpdateUser updates the user in the database. If the user is marked as deleted, it will get remove from the database.
// Also, if the user is re-enabled, all it's linked WireGuard peers will be activated again.
func (s *Server) UpdateUser(user users.User) error {
if user.DeletedAt.Valid {
return s.DeleteUser(user)
}
currentUser := s.users.GetUserUnscoped(user.Email)
// Hash user password (if set)
@@ -270,13 +281,19 @@ func (s *Server) UpdateUser(user users.User) error {
return errors.WithMessage(err, "failed to update user in manager")
}
// If user was deleted (disabled), reactivate it's peers
// Set to deleted (disabled) if user's deletedAt date is not empty
if user.DeletedAt.Valid {
return s.DeleteUser(user)
}
// Otherwise, if user was deleted (disabled), reactivate it's peers
if currentUser.DeletedAt.Valid {
for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now()
peer.DeactivatedAt = nil
peer.DeactivatedReason = ""
if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update (re)activated peer %s for %s: %v", peer.PublicKey, user.Email, err)
logrus.Errorf("failed to update (re)activated peer %s for %s: %v", peer.PublicKey, peer.Email, err)
}
}
}
@@ -284,24 +301,39 @@ func (s *Server) UpdateUser(user users.User) error {
return nil
}
// DeleteUser removes the user from the database.
// DeleteUser soft-deletes the user from the database (disable the user).
// Also, if the user has linked WireGuard peers, they will be deactivated.
func (s *Server) DeleteUser(user users.User) error {
currentUser := s.users.GetUserUnscoped(user.Email)
// Update in database
if err := s.users.DeleteUser(&user); err != nil {
if err := s.users.DeleteUser(&user, true); err != nil {
return errors.WithMessage(err, "failed to disable user in manager")
}
// Disable users peers
for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now()
peer.DeactivatedAt = &now
peer.DeactivatedReason = wireguard.DeactivatedReasonUserMissing
if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update deactivated peer %s for %s: %v", peer.PublicKey, peer.Email, err)
}
}
return nil
}
// HardDeleteUser removes the user from the database.
// Also, if the user has linked WireGuard peers, they will be deleted.
func (s *Server) HardDeleteUser(user users.User) error {
// Update in database
if err := s.users.DeleteUser(&user, false); err != nil {
return errors.WithMessage(err, "failed to delete user in manager")
}
// If user was active, disable it's peers
if !currentUser.DeletedAt.Valid {
for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now()
peer.DeactivatedAt = &now
if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update deactivated peer %s for %s: %v", peer.PublicKey, user.Email, err)
}
// remove all linked peers
for _, peer := range s.peers.GetPeersByMail(user.Email) {
if err := s.DeletePeer(peer); err != nil {
logrus.Errorf("failed to delete peer %s for %s: %v", peer.PublicKey, user.Email, err)
}
}
@@ -356,3 +388,60 @@ func (s *Server) GetDeviceNames() map[string]string {
return devNames
}
func (s *Server) RunBackgroundTasks(ctx context.Context) {
running := true
for running {
select {
case <-ctx.Done():
running = false
continue
case <-time.After(time.Duration(s.config.Core.BackgroundTaskInterval) * time.Second):
// sleep completed, select will stop blocking
}
logrus.Debug("running periodic background tasks...")
err := s.checkExpiredPeers()
if err != nil {
logrus.Errorf("failed to check expired peers: %v", err)
}
}
}
func (s *Server) checkExpiredPeers() error {
now := time.Now()
for _, devName := range s.wg.Cfg.DeviceNames {
changed := false
peers := s.peers.GetAllPeers(devName)
for _, peer := range peers {
if peer.IsExpired() && !peer.IsDeactivated() {
changed = true
peer.UpdatedAt = now
peer.DeactivatedAt = &now
peer.DeactivatedReason = wireguard.DeactivatedReasonExpired
res := s.db.Save(&peer)
if res.Error != nil {
return fmt.Errorf("failed save expired peer %s: %w", peer.PublicKey, res.Error)
}
err := s.wg.RemovePeer(peer.DeviceName, peer.PublicKey)
if err != nil {
return fmt.Errorf("failed to expire peer %s: %w", peer.PublicKey, err)
}
}
}
if changed {
err := s.WriteWireGuardConfigFile(devName)
if err != nil {
return fmt.Errorf("failed to persist config for interface %s: %w", devName, err)
}
}
}
return nil
}

View File

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

View File

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

View File

@@ -11,7 +11,6 @@ type UserSource string
const (
UserSourceLdap UserSource = "ldap" // LDAP / ActiveDirectory
UserSourceDatabase UserSource = "db" // sqlite / mysql database
UserSourceOIDC UserSource = "oidc" // open id connect, TODO: implement
)
type PrivateString string
@@ -29,7 +28,7 @@ type User struct {
// required fields
Email string `gorm:"primaryKey" form:"email" binding:"required,email"`
Source UserSource
IsAdmin bool
IsAdmin bool `form:"isadmin"`
// optional fields
Firstname string `form:"firstname" binding:"required"`
@@ -42,5 +41,5 @@ type User struct {
// database internal fields
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index" json:",omitempty"`
DeletedAt gorm.DeletedAt `gorm:"index" json:",omitempty" swaggertype:"string"`
}

View File

@@ -3,10 +3,11 @@ package wireguard
import "github.com/h44z/wg-portal/internal/common"
type Config struct {
DeviceNames []string `yaml:"devices" envconfig:"WG_DEVICES"` // managed devices
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
ManageIPAddresses bool `yaml:"manageIPAddresses" envconfig:"MANAGE_IPS"` // handle ip-address setup of interface
DeviceNames []string `yaml:"devices" envconfig:"WG_DEVICES"` // managed devices
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
ManageIPAddresses bool `yaml:"manageIPAddresses" envconfig:"MANAGE_IPS"` // handle ip-address setup of interface
UserManagePeers bool `yaml:"userManagePeers" envconfig:"USER_MANAGE_PEERS"` // user can manage own peers
}
func (c Config) GetDefaultDeviceName() string {

View File

@@ -23,9 +23,19 @@ import (
"gorm.io/gorm"
)
//
const (
DeactivatedReasonExpired = "expired"
DeactivatedReasonUserEdit = "user edit action"
DeactivatedReasonUserCreate = "user create action"
DeactivatedReasonAdminEdit = "admin edit action"
DeactivatedReasonAdminCreate = "admin create action"
DeactivatedReasonApiEdit = "api edit action"
DeactivatedReasonApiCreate = "api create action"
DeactivatedReasonLdapMissing = "missing in ldap"
DeactivatedReasonUserMissing = "missing user"
)
// CUSTOM VALIDATORS ----------------------------------------------------------------------------
//
var cidrList validator.Func = func(fl validator.FieldLevel) bool {
cidrListStr := fl.Field().String()
@@ -39,6 +49,22 @@ var cidrList validator.Func = func(fl validator.FieldLevel) bool {
return true
}
var dnsList validator.Func = func(fl validator.FieldLevel) bool {
dnsListStr := fl.Field().String()
dnsList := common.ParseStringList(dnsListStr)
validate := binding.Validator.Engine().(*validator.Validate)
for i := range dnsList {
ip := net.ParseIP(dnsList[i])
if ip == nil {
err := validate.Var(dnsList[i], "fqdn")
if err != nil {
return false
}
}
}
return true
}
var ipList validator.Func = func(fl validator.FieldLevel) bool {
ipListStr := fl.Field().String()
ipList := common.ParseStringList(ipListStr)
@@ -55,6 +81,7 @@ func init() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("cidrlist", cidrList)
_ = v.RegisterValidation("iplist", ipList)
_ = v.RegisterValidation("dnsList", dnsList)
}
}
@@ -66,9 +93,9 @@ type Peer struct {
Peer *wgtypes.Peer `gorm:"-" json:"-"` // WireGuard peer
Config string `gorm:"-" json:"-"`
UID string `form:"uid" binding:"required,alphanum" json:"-"` // uid for html identification
UID string `form:"uid" binding:"required,alphanum"` // uid for html identification
DeviceName string `gorm:"index" form:"device" binding:"required"`
DeviceType DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server" json:"-"`
DeviceType DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server"`
Identifier string `form:"identifier" binding:"required,max=64"` // Identifier AND Email make a WireGuard peer unique
Email string `gorm:"index" form:"mail" binding:"required,email"`
IgnoreGlobalSettings bool `form:"ignoreglobalsettings"`
@@ -89,15 +116,19 @@ type Peer struct {
// Misc. WireGuard Settings
PrivateKey string `form:"privkey" binding:"omitempty,base64"`
IPsStr string `form:"ip" binding:"cidrlist,required_if=DeviceType server"` // a comma separated list of IPs of the client
DNSStr string `form:"dns" binding:"iplist"` // comma separated list of the DNS servers for the client
DNSStr string `form:"dns" binding:"dnsList"` // comma separated list of the DNS servers for the client
// Global Device Settings (can be ignored, only make sense if device is in server mode)
Mtu int `form:"mtu" binding:"gte=0,lte=1500"`
DeactivatedAt *time.Time `json:",omitempty"`
CreatedBy string
UpdatedBy string
CreatedAt time.Time
UpdatedAt time.Time
DeactivatedAt *time.Time `json:",omitempty"`
DeactivatedReason string `json:",omitempty"`
ExpiresAt *time.Time `json:",omitempty" form:"expires_at" binding:"omitempty" time_format:"2006-01-02"`
CreatedBy string
UpdatedBy string
CreatedAt time.Time
UpdatedAt time.Time
}
func (p *Peer) SetIPAddresses(addresses ...string) {
@@ -223,6 +254,33 @@ func (p Peer) IsValid() bool {
return true
}
func (p Peer) WillExpire() bool {
if p.ExpiresAt == nil {
return false
}
if p.DeactivatedAt != nil {
return false // already deactivated...
}
if p.ExpiresAt.After(time.Now()) {
return true
}
return false
}
func (p Peer) IsExpired() bool {
if p.ExpiresAt == nil {
return false
}
if p.ExpiresAt.Before(time.Now()) {
return true
}
return false
}
func (p Peer) IsDeactivated() bool {
return p.DeactivatedAt != nil
}
func (p Peer) GetConfigFileName() string {
reg := regexp.MustCompile("[^a-zA-Z0-9_-]+")
return reg.ReplaceAllString(strings.ReplaceAll(p.Identifier, " ", "-"), "") + ".conf"
@@ -244,7 +302,7 @@ type Device struct {
Peers []Peer `gorm:"foreignKey:DeviceName" binding:"-" json:"-"` // linked WireGuard peers
Type DeviceType `form:"devicetype" binding:"required,oneof=client server"`
DeviceName string `form:"device" gorm:"primaryKey" binding:"required,alphanum"`
DeviceName string `form:"device" gorm:"primaryKey" binding:"required" validator:"regexp=[0-9a-zA-Z\\-]+"`
DisplayName string `form:"displayname" binding:"omitempty,max=200"`
// Core WireGuard Settings (Interface section)
@@ -255,7 +313,7 @@ type Device struct {
PublicKey string `form:"pubkey" binding:"required,base64"`
Mtu int `form:"mtu" binding:"gte=0,lte=1500"` // the interface MTU, wg-quick addition
IPsStr string `form:"ip" binding:"required,cidrlist"` // comma separated list of the IPs of the client, wg-quick addition
DNSStr string `form:"dns" binding:"iplist"` // comma separated list of the DNS servers of the client, wg-quick addition
DNSStr string `form:"dns" binding:"dnsList"` // comma separated list of the DNS servers of the client, wg-quick addition
RoutingTable string `form:"routingtable"` // the routing table, wg-quick addition
PreUp string `form:"preup"` // pre up script, wg-quick addition
PostUp string `form:"postup"` // post up script, wg-quick addition
@@ -338,12 +396,13 @@ func (d Device) GetConfig() wgtypes.Config {
return cfg
}
func (d Device) GetConfigFile(peers []Peer) ([]byte, error) {
func (d Device) GetConfigFile(peers []Peer, friendlyNames bool) ([]byte, error) {
var tplBuff bytes.Buffer
err := templateCache.ExecuteTemplate(&tplBuff, "interface.tpl", gin.H{
"Peers": peers,
"Interface": d,
"Peers": peers,
"Interface": d,
"FriendlyNames": friendlyNames,
})
if err != nil {
return nil, errors.Wrap(err, "failed to execute server template")

View File

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

View File

@@ -5,7 +5,8 @@
[Interface]
# Core settings
PrivateKey = {{ .Peer.PrivateKey }}
PrivateKey = {{or .Peer.PrivateKey "<please-insert-your-private-key>" }}
Address = {{ .Peer.IPsStr }}
# Misc. settings (optional)

59
tests/README.md Normal file
View File

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

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

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

View File

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

214
tests/pytest_UI.py Normal file
View File

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

484
tests/test_API.py Normal file
View File

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