Compare commits
	
		
			131 Commits
		
	
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								 | 
						2fdafd34ca | |
| 
							
							
								 | 
						92f5b5c8b1 | |
| 
							
							
								 | 
						8f3433b714 | |
| 
							
							
								 | 
						7856ce9555 | |
| 
							
							
								 | 
						84032d1e06 | |
| 
							
							
								 | 
						ecea82d172 | |
| 
							
							
								 | 
						73108f7f21 | |
| 
							
							
								 | 
						99104e4295 | |
| 
							
							
								 | 
						c5f52173e0 | |
| 
							
							
								 | 
						a22e807d2a | |
| 
							
							
								 | 
						fa33d3f66e | |
| 
							
							
								 | 
						46b09348e3 | |
| 
							
							
								 | 
						b3c22aa81f | |
| 
							
							
								 | 
						769883f020 | |
| 
							
							
								 | 
						45849a2aee | |
| 
							
							
								 | 
						8cfe9a3d5b | |
| 
							
							
								 | 
						4ffd7319f8 | |
| 
							
							
								 | 
						41bf0bc92c | |
| 
							
							
								 | 
						841db62347 | |
| 
							
							
								 | 
						37f459c535 | |
| 
							
							
								 | 
						a8e96f5457 | |
| 
							
							
								 | 
						867aa1305d | |
| 
							
							
								 | 
						c2a6ced991 | |
| 
							
							
								 | 
						cb118f4dea | |
| 
							
							
								 | 
						a9be53899c | |
| 
							
							
								 | 
						e73047b14f | |
| 
							
							
								 | 
						47fac2b49b | |
| 
							
							
								 | 
						c8623082fe | |
| 
							
							
								 | 
						af7742bfb3 | |
| 
							
							
								 | 
						e2e1159ef4 | |
| 
							
							
								 | 
						585b55c2ee | |
| 
							
							
								 | 
						13a4c05ff5 | |
| 
							
							
								 | 
						a06bce88e0 | |
| 
							
							
								 | 
						3024d36d76 | |
| 
							
							
								 | 
						aac9ba8b50 | |
| 
							
							
								 | 
						ece1c76aec | |
| 
							
							
								 | 
						d2b67277e3 | |
| 
							
							
								 | 
						393f896616 | |
| 
							
							
								 | 
						b55543f424 | |
| 
							
							
								 | 
						364a43e3dc | |
| 
							
							
								 | 
						7488f283c4 | |
| 
							
							
								 | 
						6bbe230fe8 | |
| 
							
							
								 | 
						28f3e820f0 | |
| 
							
							
								 | 
						b9e5ddf194 | |
| 
							
							
								 | 
						39324c5cf9 | |
| 
							
							
								 | 
						346e3bd3b8 | |
| 
							
							
								 | 
						8ac33a0278 | |
| 
							
							
								 | 
						5183bb5093 | |
| 
							
							
								 | 
						745141c752 | |
| 
							
							
								 | 
						19527ef1e0 | |
| 
							
							
								 | 
						86e52c5868 | |
| 
							
							
								 | 
						f3ed766bc4 | |
| 
							
							
								 | 
						e9357d83e2 | |
| 
							
							
								 | 
						59133327de | |
| 
							
							
								 | 
						ec757286c5 | |
| 
							
							
								 | 
						cfbdae7abb | |
| 
							
							
								 | 
						ac99317ba3 | |
| 
							
							
								 | 
						5e0217db04 | |
| 
							
							
								 | 
						d3c47c53c8 | |
| 
							
							
								 | 
						00f7f3d280 | |
| 
							
							
								 | 
						4fc52b62d2 | |
| 
							
							
								 | 
						e3e3639443 | |
| 
							
							
								 | 
						c8240fe157 | |
| 
							
							
								 | 
						abef29bf17 | |
| 
							
							
								 | 
						814093cdd3 | |
| 
							
							
								 | 
						b80c44af43 | |
| 
							
							
								 | 
						b8341dd36f | |
| 
							
							
								 | 
						7b848c841f | |
| 
							
							
								 | 
						3d59c7d0de | |
| 
							
							
								 | 
						d1cf0ca7eb | |
| 
							
							
								 | 
						9f20fe6c09 | |
| 
							
							
								 | 
						6dd5590940 | |
| 
							
							
								 | 
						aadf099f50 | |
| 
							
							
								 | 
						64b8eba092 | |
| 
							
							
								 | 
						40a0ba859e | |
| 
							
							
								 | 
						cc285c5c20 | |
| 
							
							
								 | 
						a866977cb0 | |
| 
							
							
								 | 
						ccffe4028a | |
| 
							
							
								 | 
						227e51b9cf | |
| 
							
							
								 | 
						f256668a99 | |
| 
							
							
								 | 
						e76c573b1d | |
| 
							
							
								 | 
						310c8343d3 | |
| 
							
							
								 | 
						82c8fe9926 | |
| 
							
							
								 | 
						86e8ad41cb | |
| 
							
							
								 | 
						24a0a9f5ee | |
| 
							
							
								 | 
						ea55b36a6f | |
| 
							
							
								 | 
						4b0a0d9061 | |
| 
							
							
								 | 
						99b586f1f7 | |
| 
							
							
								 | 
						d32064dd0e | |
| 
							
							
								 | 
						de6ad05577 | |
| 
							
							
								 | 
						be2ffba417 | |
| 
							
							
								 | 
						a80741e748 | |
| 
							
							
								 | 
						9d2dd711a1 | |
| 
							
							
								 | 
						033bea6fb1 | |
| 
							
							
								 | 
						c8189bb969 | |
| 
							
							
								 | 
						eab8d55d63 | |
| 
							
							
								 | 
						887bc778df | |
| 
							
							
								 | 
						63d6e1f391 | |
| 
							
							
								 | 
						2c2db61158 | |
| 
							
							
								 | 
						29b017f277 | |
| 
							
							
								 | 
						0a33ab35b6 | |
| 
							
							
								 | 
						efbc36d61f | |
| 
							
							
								 | 
						1c6fb6a424 | |
| 
							
							
								 | 
						ec7db055c8 | |
| 
							
							
								 | 
						3143268465 | |
| 
							
							
								 | 
						031d2cb7e8 | |
| 
							
							
								 | 
						97652be545 | |
| 
							
							
								 | 
						f43c59c043 | |
| 
							
							
								 | 
						ced211a822 | |
| 
							
							
								 | 
						203fe927fd | |
| 
							
							
								 | 
						6bc9a5fd87 | |
| 
							
							
								 | 
						569eaaee37 | |
| 
							
							
								 | 
						87b08a8f7c | |
| 
							
							
								 | 
						90bb2851bf | |
| 
							
							
								 | 
						a5fdb1ad1d | |
| 
							
							
								 | 
						100c4ee1f4 | |
| 
							
							
								 | 
						ad4ca4d9bb | |
| 
							
							
								 | 
						0224e1f137 | |
| 
							
							
								 | 
						037a6c56d3 | |
| 
							
							
								 | 
						4be3a65691 | |
| 
							
							
								 | 
						ef552aaed4 | |
| 
							
							
								 | 
						71ede02e1c | |
| 
							
							
								 | 
						af62be378f | |
| 
							
							
								 | 
						da99259f55 | |
| 
							
							
								 | 
						341f9b6a42 | |
| 
							
							
								 | 
						f3a788e3a4 | |
| 
							
							
								 | 
						65db3671a6 | |
| 
							
							
								 | 
						1da9b8418e | |
| 
							
							
								 | 
						edba46d2e1 | |
| 
							
							
								 | 
						e74a3a808f | |
| 
							
							
								 | 
						7f37d5f03b | 
| 
						 | 
				
			
			@ -25,3 +25,6 @@ docker-compose*
 | 
			
		|||
db
 | 
			
		||||
assets
 | 
			
		||||
wireguard-ui
 | 
			
		||||
 | 
			
		||||
# Examples
 | 
			
		||||
examples
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
*.html linguist-detectable=false
 | 
			
		||||
| 
						 | 
				
			
			@ -39,7 +39,7 @@ markComment: >
 | 
			
		|||
limitPerRun: 30
 | 
			
		||||
 | 
			
		||||
# Limit to only `issues` or `pulls`
 | 
			
		||||
# only: issues
 | 
			
		||||
only: issues
 | 
			
		||||
 | 
			
		||||
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
 | 
			
		||||
# pulls:
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +49,9 @@ limitPerRun: 30
 | 
			
		|||
#     recent activity. It will be closed if no further activity occurs. Thank you
 | 
			
		||||
#     for your contributions.
 | 
			
		||||
 | 
			
		||||
# issues:
 | 
			
		||||
#   exemptLabels:
 | 
			
		||||
#     - confirmed
 | 
			
		||||
issues:
 | 
			
		||||
 exemptLabels:
 | 
			
		||||
   - enhancement
 | 
			
		||||
   - feature request
 | 
			
		||||
   - documentation
 | 
			
		||||
   - bug
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,97 @@
 | 
			
		|||
name: Build container images
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - "master"
 | 
			
		||||
    tags:
 | 
			
		||||
      - "*"
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build-image:
 | 
			
		||||
    runs-on: ubuntu-22.04
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      # set environment
 | 
			
		||||
      - name: Set BUILD_TIME env
 | 
			
		||||
        run: echo "BUILD_TIME=$(date)" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
      - name: Set GIT_COMMIT env
 | 
			
		||||
        run: echo "GIT_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
      - name: Environment printer
 | 
			
		||||
        uses: managedkaos/print-env@v1.0
 | 
			
		||||
 | 
			
		||||
      - name: Prepare image tags
 | 
			
		||||
        id: image-tags
 | 
			
		||||
        run: |
 | 
			
		||||
          base=ngoduykhanh/wireguard-ui
 | 
			
		||||
          app_version=dev
 | 
			
		||||
 | 
			
		||||
          ## Set git tag as image tag
 | 
			
		||||
          ##
 | 
			
		||||
          if [[ '${{ github.ref }}' == *"refs/tags/"* ]]; then
 | 
			
		||||
            github_tag="${GITHUB_REF#refs/*/}"
 | 
			
		||||
            app_version=${github_tag}
 | 
			
		||||
 | 
			
		||||
            SEMVER_REGEX="^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(\\-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
 | 
			
		||||
            if [[ "$github_tag" =~ $SEMVER_REGEX ]]; then
 | 
			
		||||
              github_tag=$(echo "${github_tag}" | sed 's/^v//')
 | 
			
		||||
            fi
 | 
			
		||||
 | 
			
		||||
            container_images=$(cat <<END_HEREDOC
 | 
			
		||||
          ${base}:${github_tag}
 | 
			
		||||
          END_HEREDOC
 | 
			
		||||
          )
 | 
			
		||||
          
 | 
			
		||||
          ## Set 'latest' image tag if 'main' or 'master'
 | 
			
		||||
          ## branch is pushed
 | 
			
		||||
          ##
 | 
			
		||||
          elif [[ '${{ github.ref }}' == 'refs/heads/master' || '${{ github.ref }}' == 'refs/heads/main' ]]; then
 | 
			
		||||
            container_images=$(cat <<END_HEREDOC
 | 
			
		||||
          ${base}:latest
 | 
			
		||||
          END_HEREDOC
 | 
			
		||||
            )
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          ## Print tags for debugging purpose
 | 
			
		||||
          ##
 | 
			
		||||
          echo "[INFO] container_images: ${container_images}"
 | 
			
		||||
          
 | 
			
		||||
          ## Set container_images output
 | 
			
		||||
          ##
 | 
			
		||||
          echo "container_images<<EOF" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "$container_images" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "EOF" >> $GITHUB_OUTPUT
 | 
			
		||||
          
 | 
			
		||||
          ## Set APP_VERSION env
 | 
			
		||||
          #
 | 
			
		||||
          echo "APP_VERSION=${app_version}" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
      # set up docker and build images
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v3
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3
 | 
			
		||||
 | 
			
		||||
      - name: Login to Docker Hub
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
 | 
			
		||||
      - name: Build and push
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          push: true
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm/v7,linux/arm64
 | 
			
		||||
          tags: ${{ steps.image-tags.outputs.container_images }}
 | 
			
		||||
          build-args: |
 | 
			
		||||
            APP_VERSION=${{ env.APP_VERSION }}
 | 
			
		||||
            BUILD_TIME=${{ env.BUILD_TIME }}
 | 
			
		||||
            GIT_COMMIT=${{ env.GIT_COMMIT }}
 | 
			
		||||
          cache-from: type=gha
 | 
			
		||||
          cache-to: type=gha,mode=max
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
name: Lint
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - master
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches:
 | 
			
		||||
      - master
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  contents: read
 | 
			
		||||
  pull-requests: read
 | 
			
		||||
  checks: write
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  lint:
 | 
			
		||||
    name: Lint
 | 
			
		||||
    runs-on: ubuntu-22.04
 | 
			
		||||
    timeout-minutes: 10
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - uses: actions/setup-go@v3
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: "1.21"
 | 
			
		||||
 | 
			
		||||
      - name: golangci-lint
 | 
			
		||||
        uses: golangci/golangci-lint-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          version: v1.54
 | 
			
		||||
| 
						 | 
				
			
			@ -7,8 +7,9 @@ on:
 | 
			
		|||
jobs:
 | 
			
		||||
  releases-matrix:
 | 
			
		||||
    name: Release Go Binary
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    runs-on: ubuntu-22.04
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        # build and publish in parallel: linux/386, linux/amd64, darwin/386, darwin/amd64
 | 
			
		||||
        goos: [linux, freebsd, darwin]
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +25,7 @@ jobs:
 | 
			
		|||
          - 7
 | 
			
		||||
    steps:
 | 
			
		||||
    # get the source code
 | 
			
		||||
    - uses: actions/checkout@v2
 | 
			
		||||
    - uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
    # set environment
 | 
			
		||||
    - name: Set APP_VERSION env
 | 
			
		||||
| 
						 | 
				
			
			@ -35,25 +36,17 @@ jobs:
 | 
			
		|||
      uses: managedkaos/print-env@v1.0
 | 
			
		||||
 | 
			
		||||
    # setup node
 | 
			
		||||
    - uses: actions/setup-node@v2
 | 
			
		||||
    - uses: actions/setup-node@v4
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: '14'
 | 
			
		||||
        node-version: '20'
 | 
			
		||||
        registry-url: 'https://registry.npmjs.org'
 | 
			
		||||
 | 
			
		||||
    # prepare assets for go rice
 | 
			
		||||
    # prepare assets
 | 
			
		||||
    - name: Prepare assets
 | 
			
		||||
      run: |
 | 
			
		||||
        chmod +x ./prepare_assets.sh
 | 
			
		||||
        ./prepare_assets.sh
 | 
			
		||||
 | 
			
		||||
    # get go rice tool
 | 
			
		||||
    - name: Get go rice tool
 | 
			
		||||
      run: go get github.com/GeertJohan/go.rice/rice
 | 
			
		||||
 | 
			
		||||
    # run go rice embed
 | 
			
		||||
    - name: Run go rice embed
 | 
			
		||||
      run: ${HOME}/go/bin/rice embed-go
 | 
			
		||||
 | 
			
		||||
    # build and make the releases
 | 
			
		||||
    - name: Build and make the releases
 | 
			
		||||
      uses: wangyoucao577/go-release-action@master
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +54,8 @@ jobs:
 | 
			
		|||
        github_token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
        goos: ${{ matrix.goos }}
 | 
			
		||||
        goarch: ${{ matrix.goarch }}
 | 
			
		||||
        goversion: "https://dl.google.com/go/go1.16.1.linux-amd64.tar.gz"
 | 
			
		||||
        goversion: "https://dl.google.com/go/go1.21.5.linux-amd64.tar.gz"
 | 
			
		||||
        pre_command: export CGO_ENABLED=0
 | 
			
		||||
        binary_name: "wireguard-ui"
 | 
			
		||||
        build_flags: -v
 | 
			
		||||
        ldflags: -X "main.appVersion=${{ env.APP_VERSION }}" -X "main.buildTime=${{ env.BUILD_TIME }}" -X main.gitCommit=${{ github.sha }} -X main.gitRef=${{ github.ref }}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,10 +14,17 @@ wireguard-ui
 | 
			
		|||
 | 
			
		||||
# Dependency directories and files (remove the comment below to include it)
 | 
			
		||||
vendor/
 | 
			
		||||
assets/
 | 
			
		||||
assets/*
 | 
			
		||||
!assets/.gitkeep
 | 
			
		||||
node_modules/
 | 
			
		||||
rice-box.go
 | 
			
		||||
 | 
			
		||||
# IDEs
 | 
			
		||||
.vscode
 | 
			
		||||
.idea
 | 
			
		||||
 | 
			
		||||
# Vim
 | 
			
		||||
.*.sw[op]
 | 
			
		||||
 | 
			
		||||
# Examples
 | 
			
		||||
examples/docker-compose/config
 | 
			
		||||
examples/docker-compose/db
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
run:
 | 
			
		||||
  timeout: 5m
 | 
			
		||||
  skip-dirs:
 | 
			
		||||
    - .github
 | 
			
		||||
    - hack
 | 
			
		||||
    - vendor
 | 
			
		||||
linters:
 | 
			
		||||
  disable-all: true
 | 
			
		||||
  enable:
 | 
			
		||||
    - gofmt
 | 
			
		||||
    - revive
 | 
			
		||||
    - goimports
 | 
			
		||||
    - govet
 | 
			
		||||
    - unused
 | 
			
		||||
    - whitespace
 | 
			
		||||
    - misspell
 | 
			
		||||
  fast: false
 | 
			
		||||
linters-settings:
 | 
			
		||||
  gofmt:
 | 
			
		||||
    simplify: false
 | 
			
		||||
  revive:
 | 
			
		||||
    rules:
 | 
			
		||||
      - name: exported
 | 
			
		||||
        disabled: true
 | 
			
		||||
issues:
 | 
			
		||||
  exclude-use-default: false
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,67 @@
 | 
			
		|||
# Contributing Guidelines
 | 
			
		||||
 | 
			
		||||
Thank you for your interest in contributing to my project. Whether it's a bug report, new feature, correction, or additional
 | 
			
		||||
documentation, I greatly value feedback and contributions from my community.
 | 
			
		||||
 | 
			
		||||
Please read through this document before submitting any issues or pull requests to ensure I have all the necessary
 | 
			
		||||
information to effectively respond to your bug report or contribution.
 | 
			
		||||
 | 
			
		||||
## Reporting Bugs/Feature Requests
 | 
			
		||||
 | 
			
		||||
I welcome you to use the GitHub issue tracker to report bugs or suggest features.
 | 
			
		||||
 | 
			
		||||
When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
 | 
			
		||||
reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
 | 
			
		||||
 | 
			
		||||
- A reproducible test case or series of steps
 | 
			
		||||
- The version of my code being used
 | 
			
		||||
- Any modifications you've made relevant to the bug
 | 
			
		||||
- Anything unusual about your environment or deployment
 | 
			
		||||
 | 
			
		||||
## Contributing via Pull Requests
 | 
			
		||||
 | 
			
		||||
### Discussion of New Features
 | 
			
		||||
Before initiating the implementation of a new feature, I encourage contributors to open a discussion by creating a new GitHub issue. This allows me to provide feedback, share insights, and ensure alignment with the project's direction and save your time.
 | 
			
		||||
 | 
			
		||||
#### Process for Discussing New Features:
 | 
			
		||||
 | 
			
		||||
1. **Create an Issue:**
 | 
			
		||||
   - Go to the "Issues" tab in the repository.
 | 
			
		||||
   - Click on "New Issue."
 | 
			
		||||
   - Clearly describe the proposed feature, its purpose, and potential benefits.
 | 
			
		||||
 | 
			
		||||
2. **Engage in Discussion:**
 | 
			
		||||
   - Respond promptly to comments and feedback from the community.
 | 
			
		||||
   - Be open to adjusting the feature based on collaborative input.
 | 
			
		||||
 | 
			
		||||
3. **Consensus Building:**
 | 
			
		||||
   - Strive to reach a consensus on the proposed feature.
 | 
			
		||||
   - Ensure alignment with the overall project vision.
 | 
			
		||||
 | 
			
		||||
### Bug Fixes and Improvements
 | 
			
		||||
 | 
			
		||||
For bug fixes, documentation improvements, and general enhancements, feel free to submit a pull request directly.
 | 
			
		||||
 | 
			
		||||
#### Pull Request Guidelines:
 | 
			
		||||
 | 
			
		||||
1. **Fork the Repository:**
 | 
			
		||||
   - Fork the repository to your GitHub account.
 | 
			
		||||
 | 
			
		||||
2. **Create a Branch:**
 | 
			
		||||
   - Create a new branch for your changes.
 | 
			
		||||
 | 
			
		||||
3. **Make Changes:**
 | 
			
		||||
   - Make your changes and ensure they adhere to coding standards.
 | 
			
		||||
 | 
			
		||||
4. **Submit a Pull Request:**
 | 
			
		||||
   - Submit a pull request to the main repository.
 | 
			
		||||
 | 
			
		||||
5. **Engage in Review:**
 | 
			
		||||
   - Be responsive to feedback and address any requested changes.
 | 
			
		||||
 | 
			
		||||
6. **Merge Process:**
 | 
			
		||||
   - Once approved, your changes will be merged into the main branch.
 | 
			
		||||
 | 
			
		||||
## Licensing
 | 
			
		||||
 | 
			
		||||
See the [LICENSE](LICENSE) file for my project's licensing.
 | 
			
		||||
							
								
								
									
										41
									
								
								Dockerfile
								
								
								
								
							
							
						
						
									
										41
									
								
								Dockerfile
								
								
								
								
							| 
						 | 
				
			
			@ -1,6 +1,14 @@
 | 
			
		|||
# Build stage
 | 
			
		||||
FROM golang:1.16.7-alpine3.14 as builder
 | 
			
		||||
LABEL maintainer="Khanh Ngo <k@ndk.name"
 | 
			
		||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.21-alpine3.19 AS builder
 | 
			
		||||
LABEL maintainer="Khanh Ngo <k@ndk.name>"
 | 
			
		||||
 | 
			
		||||
ARG BUILDPLATFORM
 | 
			
		||||
ARG TARGETOS
 | 
			
		||||
ARG TARGETARCH
 | 
			
		||||
ARG APP_VERSION=dev
 | 
			
		||||
ARG BUILD_TIME
 | 
			
		||||
ARG GIT_COMMIT
 | 
			
		||||
 | 
			
		||||
ARG BUILD_DEPENDENCIES="npm \
 | 
			
		||||
    yarn"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -9,8 +17,11 @@ RUN apk add --update --no-cache ${BUILD_DEPENDENCIES}
 | 
			
		|||
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
 | 
			
		||||
# Add sources
 | 
			
		||||
COPY . /build
 | 
			
		||||
# Add dependencies
 | 
			
		||||
COPY go.mod /build
 | 
			
		||||
COPY go.sum /build
 | 
			
		||||
COPY package.json /build
 | 
			
		||||
COPY yarn.lock /build
 | 
			
		||||
 | 
			
		||||
# Prepare assets
 | 
			
		||||
RUN yarn install --pure-lockfile --production && \
 | 
			
		||||
| 
						 | 
				
			
			@ -35,34 +46,32 @@ RUN mkdir -p assets/plugins && \
 | 
			
		|||
    /build/node_modules/jquery-tags-input/ \
 | 
			
		||||
    assets/plugins/
 | 
			
		||||
 | 
			
		||||
# Add sources
 | 
			
		||||
COPY . /build
 | 
			
		||||
 | 
			
		||||
# Move custom assets
 | 
			
		||||
RUN cp -r /build/custom/ assets/
 | 
			
		||||
 | 
			
		||||
# Get go modules and build tool
 | 
			
		||||
RUN go mod download && \
 | 
			
		||||
    go get github.com/GeertJohan/go.rice/rice
 | 
			
		||||
 | 
			
		||||
# Build
 | 
			
		||||
RUN rice embed-go && \
 | 
			
		||||
    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o wg-ui .
 | 
			
		||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X 'main.appVersion=${APP_VERSION}' -X 'main.buildTime=${BUILD_TIME}' -X 'main.gitCommit=${GIT_COMMIT}'" -a -o wg-ui .
 | 
			
		||||
 | 
			
		||||
# Release stage
 | 
			
		||||
FROM alpine:3.11
 | 
			
		||||
FROM alpine:3.19
 | 
			
		||||
 | 
			
		||||
RUN addgroup -S wgui && \
 | 
			
		||||
    adduser -S -D -G wgui wgui
 | 
			
		||||
 | 
			
		||||
RUN apk --no-cache add ca-certificates
 | 
			
		||||
RUN apk --no-cache add ca-certificates wireguard-tools jq iptables
 | 
			
		||||
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
RUN mkdir -p db
 | 
			
		||||
 | 
			
		||||
# Copy binary files
 | 
			
		||||
COPY --from=builder --chown=wgui:wgui /build/wg-ui /app
 | 
			
		||||
 | 
			
		||||
COPY --from=builder --chown=wgui:wgui /build/wg-ui .
 | 
			
		||||
RUN chmod +x wg-ui
 | 
			
		||||
COPY init.sh .
 | 
			
		||||
RUN chmod +x init.sh
 | 
			
		||||
 | 
			
		||||
EXPOSE 5000/tcp
 | 
			
		||||
HEALTHCHECK CMD ["wget","--output-document=-","--quiet","--tries=1","http://127.0.0.1:5000/_health"]
 | 
			
		||||
ENTRYPOINT ["./wg-ui"]
 | 
			
		||||
ENTRYPOINT ["./init.sh"]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										208
									
								
								README.md
								
								
								
								
							
							
						
						
									
										208
									
								
								README.md
								
								
								
								
							| 
						 | 
				
			
			@ -5,58 +5,119 @@
 | 
			
		|||
A web user interface to manage your WireGuard setup.
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- Friendly UI
 | 
			
		||||
- Authentication
 | 
			
		||||
- Manage extra client's information (name, email, etc)
 | 
			
		||||
- Retrieve configs using QR code / file
 | 
			
		||||
- Manage extra client information (name, email, etc.)
 | 
			
		||||
- Retrieve client config using QR code / file / email / Telegram
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## Run WireGuard-UI
 | 
			
		||||
 | 
			
		||||
Default username and password are `admin`.
 | 
			
		||||
 | 
			
		||||
### Using docker compose
 | 
			
		||||
 | 
			
		||||
You can take a look at this example of [docker-compose.yml](https://github.com/ngoduykhanh/wireguard-ui/blob/master/docker-compose.yaml). Please adjust volume mount points to work with your setup. Then run it like below:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
docker-compose up
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Note:
 | 
			
		||||
 | 
			
		||||
There is a Status option that needs docker to be able to access the network of the host in order to read the 
 | 
			
		||||
wireguard interface stats. See the `cap_add` and `network_mode` options on the docker-compose.yaml
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Environment Variables
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Set the `SESSION_SECRET` environment variable to a random value.
 | 
			
		||||
 | 
			
		||||
In order to sent the wireguard configuration to clients via email (using sendgrid api) set the following environment variables
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
SENDGRID_API_KEY: Your sendgrid api key
 | 
			
		||||
EMAIL_FROM_ADDRESS: the email address you registered on sendgrid
 | 
			
		||||
EMAIL_FROM_NAME: the sender's email address
 | 
			
		||||
```
 | 
			
		||||
> ⚠️The default username and password are `admin`. Please change it to secure your setup.
 | 
			
		||||
 | 
			
		||||
### Using binary file
 | 
			
		||||
 | 
			
		||||
Download the binary file from the release and run it with command:
 | 
			
		||||
Download the binary file from the release page and run it directly on the host machine
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
./wireguard-ui
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Auto restart WireGuard daemon
 | 
			
		||||
WireGuard-UI only takes care of configuration generation. You can use systemd to watch for the changes and restart the service. Following is an example:
 | 
			
		||||
### Using docker compose
 | 
			
		||||
 | 
			
		||||
### systemd
 | 
			
		||||
 | 
			
		||||
Create /etc/systemd/system/wgui.service
 | 
			
		||||
The [examples/docker-compose](examples/docker-compose) folder contains example docker-compose files.
 | 
			
		||||
Choose the example which fits you the most, adjust the configuration for your needs, then run it like below:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
docker-compose up
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Environment Variables
 | 
			
		||||
 | 
			
		||||
| Variable                      | Description                                                                                                                                                                                                                                                                         | Default                            |
 | 
			
		||||
|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|
 | 
			
		||||
| `BASE_PATH`                   | Set this variable if you run wireguard-ui under a subpath of your reverse proxy virtual host (e.g. /wireguard)                                                                                                                                                                      | N/A                                |
 | 
			
		||||
| `BIND_ADDRESS`                | The addresses that can access to the web interface and the port, use unix:///abspath/to/file.socket for unix domain socket.                                                                                                                                                         | 0.0.0.0:80                         |
 | 
			
		||||
| `SESSION_SECRET`              | The secret key used to encrypt the session cookies. Set this to a random value                                                                                                                                                                                                      | N/A                                |
 | 
			
		||||
| `SESSION_SECRET_FILE`         | Optional filepath for the secret key used to encrypt the session cookies. Leave `SESSION_SECRET` blank to take effect                                                                                                                                                               | N/A                                |
 | 
			
		||||
| `SESSION_MAX_DURATION`        | Max time in days a remembered session is refreshed and valid. Non-refreshed session is valid for 7 days max, regardless of this setting.                                                                                                                                            | 90                                 |
 | 
			
		||||
| `SUBNET_RANGES`               | The list of address subdivision ranges. Format: `SR Name:10.0.1.0/24; SR2:10.0.2.0/24,10.0.3.0/24` Each CIDR must be inside one of the server interfaces.                                                                                                                           | N/A                                |
 | 
			
		||||
| `WGUI_USERNAME`               | The username for the login page. Used for db initialization only                                                                                                                                                                                                                    | `admin`                            |
 | 
			
		||||
| `WGUI_PASSWORD`               | The password for the user on the login page. Will be hashed automatically. Used for db initialization only                                                                                                                                                                          | `admin`                            |
 | 
			
		||||
| `WGUI_PASSWORD_FILE`          | Optional filepath for the user login password. Will be hashed automatically. Used for db initialization only. Leave `WGUI_PASSWORD` blank to take effect                                                                                                                            | N/A                                |
 | 
			
		||||
| `WGUI_PASSWORD_HASH`          | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only                                                                                                                                                                 | N/A                                |
 | 
			
		||||
| `WGUI_PASSWORD_HASH_FILE`     | Optional filepath for the user login password hash. (alternative to `WGUI_PASSWORD_FILE`). Used for db initialization only. Leave `WGUI_PASSWORD_HASH` blank to take effect                                                                                                         | N/A                                |
 | 
			
		||||
| `WGUI_ENDPOINT_ADDRESS`       | The default endpoint address used in global settings where clients should connect to. The endpoint can contain a port as well, useful when you are listening internally on the `WGUI_SERVER_LISTEN_PORT` port, but you forward on another port (ex 9000). Ex: myvpn.dyndns.com:9000 | Resolved to your public ip address |
 | 
			
		||||
| `WGUI_FAVICON_FILE_PATH`      | The file path used as website favicon                                                                                                                                                                                                                                               | Embedded WireGuard logo            |
 | 
			
		||||
| `WGUI_DNS`                    | The default DNS servers (comma-separated-list) used in the global settings                                                                                                                                                                                                          | `1.1.1.1`                          |
 | 
			
		||||
| `WGUI_MTU`                    | The default MTU used in global settings                                                                                                                                                                                                                                             | `1450`                             |
 | 
			
		||||
| `WGUI_PERSISTENT_KEEPALIVE`   | The default persistent keepalive for WireGuard in global settings                                                                                                                                                                                                                   | `15`                               |
 | 
			
		||||
| `WGUI_FIREWALL_MARK`          | The default WireGuard firewall mark                                                                                                                                                                                                                                                 | `0xca6c`  (51820)                  |
 | 
			
		||||
| `WGUI_TABLE`                  | The default WireGuard table value settings                                                                                                                                                                                                                                          | `auto`                             |
 | 
			
		||||
| `WGUI_CONFIG_FILE_PATH`       | The default WireGuard config file path used in global settings                                                                                                                                                                                                                      | `/etc/wireguard/wg0.conf`          |
 | 
			
		||||
| `WGUI_LOG_LEVEL`              | The default log level. Possible values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF`                                                                                                                                                                                                     | `INFO`                             |
 | 
			
		||||
| `WG_CONF_TEMPLATE`            | The custom `wg.conf` config file template. Please refer to our [default template](https://github.com/ngoduykhanh/wireguard-ui/blob/master/templates/wg.conf)                                                                                                                        | N/A                                |
 | 
			
		||||
| `EMAIL_FROM_ADDRESS`          | The sender email address                                                                                                                                                                                                                                                            | N/A                                |
 | 
			
		||||
| `EMAIL_FROM_NAME`             | The sender name                                                                                                                                                                                                                                                                     | `WireGuard UI`                     |
 | 
			
		||||
| `SENDGRID_API_KEY`            | The SendGrid api key                                                                                                                                                                                                                                                                | N/A                                |
 | 
			
		||||
| `SENDGRID_API_KEY_FILE`       | Optional filepath for the SendGrid api key. Leave `SENDGRID_API_KEY` blank to take effect                                                                                                                                                                                           | N/A                                |
 | 
			
		||||
| `SMTP_HOSTNAME`               | The SMTP IP address or hostname                                                                                                                                                                                                                                                     | `127.0.0.1`                        |
 | 
			
		||||
| `SMTP_PORT`                   | The SMTP port                                                                                                                                                                                                                                                                       | `25`                               |
 | 
			
		||||
| `SMTP_USERNAME`               | The SMTP username                                                                                                                                                                                                                                                                   | N/A                                |
 | 
			
		||||
| `SMTP_PASSWORD`               | The SMTP user password                                                                                                                                                                                                                                                              | N/A                                |
 | 
			
		||||
| `SMTP_PASSWORD_FILE`          | Optional filepath for the SMTP user password. Leave `SMTP_PASSWORD` blank to take effect                                                                                                                                                                                            | N/A                                |
 | 
			
		||||
| `SMTP_AUTH_TYPE`              | The SMTP authentication type. Possible values: `PLAIN`, `LOGIN`, `NONE`                                                                                                                                                                                                             | `NONE`                             |
 | 
			
		||||
| `SMTP_ENCRYPTION`             | The encryption method. Possible values: `NONE`, `SSL`, `SSLTLS`, `TLS`, `STARTTLS`                                                                                                                                                                                                  | `STARTTLS`                         |
 | 
			
		||||
| `SMTP_HELO`                   | Hostname to use for the HELO message. smtp-relay.gmail.com needs this set to anything but `localhost`                                                                                                                                                                               | `localhost`                        |
 | 
			
		||||
| `TELEGRAM_TOKEN`              | Telegram bot token for distributing configs to clients                                                                                                                                                                                                                              | N/A                                |
 | 
			
		||||
| `TELEGRAM_ALLOW_CONF_REQUEST` | Allow users to get configs from the bot by sending a message                                                                                                                                                                                                                        | `false`                            |
 | 
			
		||||
| `TELEGRAM_FLOOD_WAIT`         | Time in minutes before the next conf request is processed                                                                                                                                                                                                                           | `60`                               |
 | 
			
		||||
 | 
			
		||||
### Defaults for server configuration
 | 
			
		||||
 | 
			
		||||
These environment variables are used to control the default server settings used when initializing the database.
 | 
			
		||||
 | 
			
		||||
| Variable                          | Description                                                                                   | Default         |
 | 
			
		||||
|-----------------------------------|-----------------------------------------------------------------------------------------------|-----------------|
 | 
			
		||||
| `WGUI_SERVER_INTERFACE_ADDRESSES` | The default interface addresses (comma-separated-list) for the WireGuard server configuration | `10.252.1.0/24` |
 | 
			
		||||
| `WGUI_SERVER_LISTEN_PORT`         | The default server listen port                                                                | `51820`         |
 | 
			
		||||
| `WGUI_SERVER_POST_UP_SCRIPT`      | The default server post-up script                                                             | N/A             |
 | 
			
		||||
| `WGUI_SERVER_POST_DOWN_SCRIPT`    | The default server post-down script                                                           | N/A             |
 | 
			
		||||
 | 
			
		||||
### Defaults for new clients
 | 
			
		||||
 | 
			
		||||
These environment variables are used to set the defaults used in `New Client` dialog.
 | 
			
		||||
 | 
			
		||||
| Variable                                    | Description                                                                                     | Default     |
 | 
			
		||||
|---------------------------------------------|-------------------------------------------------------------------------------------------------|-------------|
 | 
			
		||||
| `WGUI_DEFAULT_CLIENT_ALLOWED_IPS`           | Comma-separated-list of CIDRs for the `Allowed IPs` field. (default )                           | `0.0.0.0/0` |
 | 
			
		||||
| `WGUI_DEFAULT_CLIENT_EXTRA_ALLOWED_IPS`     | Comma-separated-list of CIDRs for the `Extra Allowed IPs` field. (default empty)                | N/A         |
 | 
			
		||||
| `WGUI_DEFAULT_CLIENT_USE_SERVER_DNS`        | Boolean value [`0`, `f`, `F`, `false`, `False`, `FALSE`, `1`, `t`, `T`, `true`, `True`, `TRUE`] | `true`      |
 | 
			
		||||
| `WGUI_DEFAULT_CLIENT_ENABLE_AFTER_CREATION` | Boolean value [`0`, `f`, `F`, `false`, `False`, `FALSE`, `1`, `t`, `T`, `true`, `True`, `TRUE`] | `true`      |
 | 
			
		||||
 | 
			
		||||
### Docker only
 | 
			
		||||
 | 
			
		||||
These environment variables only apply to the docker container.
 | 
			
		||||
 | 
			
		||||
| Variable              | Description                                                   | Default |
 | 
			
		||||
|-----------------------|---------------------------------------------------------------|---------|
 | 
			
		||||
| `WGUI_MANAGE_START`   | Start/stop WireGuard when the container is started/stopped    | `false` |
 | 
			
		||||
| `WGUI_MANAGE_RESTART` | Auto restart WireGuard when we Apply Config changes in the UI | `false` |
 | 
			
		||||
 | 
			
		||||
## Auto restart WireGuard daemon
 | 
			
		||||
 | 
			
		||||
WireGuard-UI only takes care of configuration generation. You can use systemd to watch for the changes and restart the
 | 
			
		||||
service. Following is an example:
 | 
			
		||||
 | 
			
		||||
### Using systemd
 | 
			
		||||
 | 
			
		||||
Create `/etc/systemd/system/wgui.service`
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
cd /etc/systemd/system/
 | 
			
		||||
cat << EOF > wgui.service
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=Restart WireGuard
 | 
			
		||||
After=network.target
 | 
			
		||||
| 
						 | 
				
			
			@ -67,11 +128,14 @@ ExecStart=/usr/bin/systemctl restart wg-quick@wg0.service
 | 
			
		|||
 | 
			
		||||
[Install]
 | 
			
		||||
RequiredBy=wgui.path
 | 
			
		||||
EOF
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Create /etc/systemd/system/wgui.path
 | 
			
		||||
Create `/etc/systemd/system/wgui.path`
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
```bash
 | 
			
		||||
cd /etc/systemd/system/
 | 
			
		||||
cat << EOF > wgui.path
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=Watch /etc/wireguard/wg0.conf for changes
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -80,81 +144,103 @@ PathModified=/etc/wireguard/wg0.conf
 | 
			
		|||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
EOF
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Apply it
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
```sh
 | 
			
		||||
systemctl enable wgui.{path,service}
 | 
			
		||||
systemctl start wgui.{path,service}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### openrc
 | 
			
		||||
### Using openrc
 | 
			
		||||
 | 
			
		||||
Create and `chmod +x` /usr/local/bin/wgui
 | 
			
		||||
```
 | 
			
		||||
Create `/usr/local/bin/wgui` file and make it executable
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
cd /usr/local/bin/
 | 
			
		||||
cat << EOF > wgui
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
wg-quick down wg0
 | 
			
		||||
wg-quick up wg0
 | 
			
		||||
EOF
 | 
			
		||||
chmod +x wgui
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Create and `chmod +x` /etc/init.d/wgui
 | 
			
		||||
```
 | 
			
		||||
Create `/etc/init.d/wgui` file and make it executable
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
cd /etc/init.d/
 | 
			
		||||
cat << EOF > wgui
 | 
			
		||||
#!/sbin/openrc-run
 | 
			
		||||
 | 
			
		||||
command=/sbin/inotifyd
 | 
			
		||||
command_args="/usr/local/bin/wgui /etc/wireguard/wg0.conf:w"
 | 
			
		||||
pidfile=/run/${RC_SVCNAME}.pid
 | 
			
		||||
command_background=yes
 | 
			
		||||
EOF
 | 
			
		||||
chmod +x wgui
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Apply it
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
```sh
 | 
			
		||||
rc-service wgui start
 | 
			
		||||
rc-update add wgui default
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Using Docker
 | 
			
		||||
 | 
			
		||||
Set `WGUI_MANAGE_RESTART=true` to manage Wireguard interface restarts.
 | 
			
		||||
Using `WGUI_MANAGE_START=true` can also replace the function of `wg-quick@wg0` service, to start Wireguard at boot, by
 | 
			
		||||
running the container with `restart: unless-stopped`. These settings can also pick up changes to Wireguard Config File
 | 
			
		||||
Path, after restarting the container. Please make sure you have `--cap-add=NET_ADMIN` in your container config to make
 | 
			
		||||
this feature work.
 | 
			
		||||
 | 
			
		||||
## Build
 | 
			
		||||
 | 
			
		||||
### Build docker image
 | 
			
		||||
 | 
			
		||||
Go to the project root directory and run the following command:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
docker build --build-arg=GIT_COMMIT=$(git rev-parse --short HEAD) -t wireguard-ui .
 | 
			
		||||
```
 | 
			
		||||
docker build -t wireguard-ui .
 | 
			
		||||
 | 
			
		||||
or
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
docker compose build --build-arg=GIT_COMMIT=$(git rev-parse --short HEAD)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
:information_source: A container image is available on [Docker Hub](https://hub.docker.com/r/ngoduykhanh/wireguard-ui)
 | 
			
		||||
which you can pull and use
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
docker pull ngoduykhanh/wireguard-ui
 | 
			
		||||
````
 | 
			
		||||
 | 
			
		||||
### Build binary file
 | 
			
		||||
 | 
			
		||||
Prepare the assets directory
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
```sh
 | 
			
		||||
./prepare_assets.sh
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then you can embed resources by generating Go source code
 | 
			
		||||
Then build your executable
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
rice embed-go
 | 
			
		||||
```sh
 | 
			
		||||
go build -o wireguard-ui
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Or, append resources to executable as zip file
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
go build -o wireguard-ui
 | 
			
		||||
rice append --exec wireguard-ui
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Screenshot
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
MIT. See [LICENSE](https://github.com/ngoduykhanh/wireguard-ui/blob/master/LICENSE).
 | 
			
		||||
 | 
			
		||||
## Support
 | 
			
		||||
 | 
			
		||||
If you like the project and want to support it, you can *buy me a coffee* ☕
 | 
			
		||||
 | 
			
		||||
<a href="https://www.buymeacoffee.com/khanhngo" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 17 KiB  | 
| 
						 | 
				
			
			@ -1,5 +1,20 @@
 | 
			
		|||
function renderClientList(data) {
 | 
			
		||||
    $.each(data, function(index, obj) {
 | 
			
		||||
        // render telegram button
 | 
			
		||||
        let telegramButton = ''
 | 
			
		||||
        if (obj.Client.telegram_userid) {
 | 
			
		||||
            telegramButton =    `<div class="btn-group">      
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal"
 | 
			
		||||
                                        data-target="#modal_telegram_client" data-clientid="${obj.Client.id}"
 | 
			
		||||
                                        data-clientname="${obj.Client.name}">Telegram</button>
 | 
			
		||||
                                </div>`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let telegramHtml = "";
 | 
			
		||||
        if (obj.Client.telegram_userid && obj.Client.telegram_userid.length > 0) {
 | 
			
		||||
            telegramHtml = `<span class="info-box-text" style="display: none"><i class="fas fa-tguserid"></i>${obj.Client.telegram_userid}</span>`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // render client status css tag style
 | 
			
		||||
        let clientStatusHtml = '>'
 | 
			
		||||
        if (obj.Client.enabled) {
 | 
			
		||||
| 
						 | 
				
			
			@ -18,40 +33,60 @@ function renderClientList(data) {
 | 
			
		|||
            allowedIpsHtml += `<small class="badge badge-secondary">${obj}</small> `;
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        let subnetRangesString = "";
 | 
			
		||||
        if (obj.Client.subnet_ranges && obj.Client.subnet_ranges.length > 0) {
 | 
			
		||||
            subnetRangesString = obj.Client.subnet_ranges.join(',')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let additionalNotesHtml = "";
 | 
			
		||||
        if (obj.Client.additional_notes && obj.Client.additional_notes.length > 0) {
 | 
			
		||||
            additionalNotesHtml = `<span class="info-box-text" style="display: none"><i class="fas fa-additional_notes"></i>${obj.Client.additional_notes.toUpperCase()}</span>`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // render client html content
 | 
			
		||||
        let html = `<div class="col-sm-6" id="client_${obj.Client.id}">
 | 
			
		||||
        let html = `<div class="col-sm-6 col-md-6 col-lg-4" id="client_${obj.Client.id}">
 | 
			
		||||
                        <div class="info-box">
 | 
			
		||||
                            <div class="overlay" id="paused_${obj.Client.id}"` + clientStatusHtml
 | 
			
		||||
                                + `<i class="paused-client fas fa-3x fa-play" onclick="resumeClient('${obj.Client.id}')"></i>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <img src="${obj.QRCode}" />
 | 
			
		||||
                            <div class="info-box-content">
 | 
			
		||||
                            <div class="info-box-content" style="overflow: hidden">
 | 
			
		||||
                                <div class="btn-group">
 | 
			
		||||
                                    <a href="/download?clientid=${obj.Client.id}" class="btn btn-outline-success btn-sm">Download</a>
 | 
			
		||||
                                    <a href="download?clientid=${obj.Client.id}" class="btn btn-outline-primary btn-sm">Download</a>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="btn-group">      
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-secondary btn-sm" data-toggle="modal"
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal"
 | 
			
		||||
                                        data-target="#modal_qr_client" data-clientid="${obj.Client.id}"
 | 
			
		||||
                                        data-clientname="${obj.Client.name}" ${obj.QRCode != "" ? '' : ' disabled'}>QR code</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="btn-group">      
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal"
 | 
			
		||||
                                        data-target="#modal_email_client" data-clientid="${obj.Client.id}"
 | 
			
		||||
                                        data-clientname="${obj.Client.name}">Email</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="btn-group">        
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal"
 | 
			
		||||
                                ${telegramButton}
 | 
			
		||||
                                <div class="btn-group">
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-danger btn-sm">More</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-danger btn-sm dropdown-toggle dropdown-icon" 
 | 
			
		||||
                                        data-toggle="dropdown">
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <div class="dropdown-menu" role="menu">
 | 
			
		||||
                                        <a class="dropdown-item" href="#" data-toggle="modal"
 | 
			
		||||
                                        data-target="#modal_edit_client" data-clientid="${obj.Client.id}"
 | 
			
		||||
                                        data-clientname="${obj.Client.name}">Edit</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="btn-group">        
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-warning btn-sm" data-toggle="modal"
 | 
			
		||||
                                        data-clientname="${obj.Client.name}">Edit</a>
 | 
			
		||||
                                        <a class="dropdown-item" href="#" data-toggle="modal"
 | 
			
		||||
                                        data-target="#modal_pause_client" data-clientid="${obj.Client.id}"
 | 
			
		||||
                                        data-clientname="${obj.Client.name}">Disable</button>
 | 
			
		||||
                                        
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="btn-group">        
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal"
 | 
			
		||||
                                        data-clientname="${obj.Client.name}">Disable</a>
 | 
			
		||||
                                        <a class="dropdown-item" href="#" data-toggle="modal"
 | 
			
		||||
                                        data-target="#modal_remove_client" data-clientid="${obj.Client.id}"
 | 
			
		||||
                                        data-clientname="${obj.Client.name}">Remove</button>
 | 
			
		||||
                                        data-clientname="${obj.Client.name}">Delete</a>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <hr>
 | 
			
		||||
                                <span class="info-box-text"><i class="fas fa-user"></i> ${obj.Client.name}</span>
 | 
			
		||||
                                <span class="info-box-text" style="display: none"><i class="fas fa-key"></i> ${obj.Client.public_key}</span>
 | 
			
		||||
                                <span class="info-box-text" style="display: none"><i class="fas fa-subnetrange"></i>${subnetRangesString}</span>
 | 
			
		||||
                                ${telegramHtml}
 | 
			
		||||
                                ${additionalNotesHtml}
 | 
			
		||||
                                <span class="info-box-text"><i class="fas fa-envelope"></i> ${obj.Client.email}</span>
 | 
			
		||||
                                <span class="info-box-text"><i class="fas fa-clock"></i>
 | 
			
		||||
                                    ${prettyDateTime(obj.Client.created_at)}</span>
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +94,8 @@ function renderClientList(data) {
 | 
			
		|||
                                    ${prettyDateTime(obj.Client.updated_at)}</span>
 | 
			
		||||
                                <span class="info-box-text"><i class="fas fa-server" style="${obj.Client.use_server_dns ? "opacity: 1.0" : "opacity: 0.5"}"></i>
 | 
			
		||||
                                    ${obj.Client.use_server_dns ? 'DNS enabled' : 'DNS disabled'}</span>
 | 
			
		||||
                                <span class="info-box-text"><i class="fas fa-file"></i>
 | 
			
		||||
                                    ${obj.Client.additional_notes}</span>
 | 
			
		||||
                                <span class="info-box-text"><strong>IP Allocation</strong></span>`
 | 
			
		||||
                                + allocatedIpsHtml
 | 
			
		||||
                                + `<span class="info-box-text"><strong>Allowed IPs</strong></span>`
 | 
			
		||||
| 
						 | 
				
			
			@ -72,6 +109,34 @@ function renderClientList(data) {
 | 
			
		|||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderUserList(data) {
 | 
			
		||||
    $.each(data, function(index, obj) {
 | 
			
		||||
        let clientStatusHtml = '>'
 | 
			
		||||
 | 
			
		||||
        // render user html content
 | 
			
		||||
        let html = `<div class="col-sm-6 col-md-6 col-lg-4" id="user_${obj.username}">
 | 
			
		||||
                        <div class="info-box">
 | 
			
		||||
                            <div class="info-box-content">
 | 
			
		||||
                                <div class="btn-group">
 | 
			
		||||
                                     <button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" data-target="#modal_edit_user" data-username="${obj.username}">Edit</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="btn-group">
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal"
 | 
			
		||||
                                        data-target="#modal_remove_user" data-username="${obj.username}">Delete</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <hr>
 | 
			
		||||
                                <span class="info-box-text"><i class="fas fa-user"></i> ${obj.username}</span>
 | 
			
		||||
                                <span class="info-box-text"><i class="fas fa-terminal"></i> ${obj.admin? 'Administrator':'Manager'}</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>`
 | 
			
		||||
 | 
			
		||||
        // add the user html elements to the list
 | 
			
		||||
        $('#users-list').append(html);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function prettyDateTime(timeStr) {
 | 
			
		||||
    const dt = new Date(timeStr);
 | 
			
		||||
    const offsetMs = dt.getTimezoneOffset() * 60 * 1000;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,210 @@
 | 
			
		|||
var base_url = jQuery(".brand-link").attr('href');
 | 
			
		||||
if (base_url.substring(base_url.length - 1, base_url.length) != "/")
 | 
			
		||||
    base_url = base_url + "/";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const wake_on_lan_new_template = '<div class="col-sm-4" id="{{ .Id }}">\n' +
 | 
			
		||||
    '\t<div class="info-box">\n' +
 | 
			
		||||
    '\t\t<div class="info-box-content">\n' +
 | 
			
		||||
    '\t\t\t<div class="btn-group">\n' +
 | 
			
		||||
    '\t\t\t\t<button type="button" class="btn btn-outline-success btn-sm"\n' +
 | 
			
		||||
    '\t\t\t\t\t\tdata-mac-address="{{ .MacAddress }}">Wake On\n' +
 | 
			
		||||
    '\t\t\t\t</button>\n' +
 | 
			
		||||
    '\t\t\t\t<button type="button"\n' +
 | 
			
		||||
    '\t\t\t\t\t\tclass="btn btn-outline-primary btn-sm btn_modify_wake_on_lan_host"\n' +
 | 
			
		||||
    '\t\t\t\t\t\tdata-toggle="modal" data-target="#modal_wake_on_lan_host"\n' +
 | 
			
		||||
    '\t\t\t\t\t\tdata-name="{{ .Name }}" data-mac-address="{{ .MacAddress }}">Edit\n' +
 | 
			
		||||
    '\t\t\t\t</button>\n' +
 | 
			
		||||
    '\t\t\t\t<button type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal"\n' +
 | 
			
		||||
    '\t\t\t\t\t\tdata-target="#modal_remove_wake_on_lan_host"\n' +
 | 
			
		||||
    '\t\t\t\t\t\tdata-mac-address="{{ .MacAddress }}">Remove\n' +
 | 
			
		||||
    '\t\t\t\t</button>\n' +
 | 
			
		||||
    '\t\t\t</div>\n' +
 | 
			
		||||
    '\t\t\t<hr>\n' +
 | 
			
		||||
    '\t\t\t<span class="info-box-text"><i class="fas fa-address-card"></i> <span class="name">{{ .Name }}</span></span>\n' +
 | 
			
		||||
    '\t\t\t<span class="info-box-text"><i class="fas fa-ethernet"></i> <span class="mac-address">{{ .MacAddress }}</span></span>\n' +
 | 
			
		||||
    '\t\t\t<span class="info-box-text"><i class="fas fa-clock"></i> <span class="latest-used">Unused</span></span>\n' +
 | 
			
		||||
    '\t\t</div>\n' +
 | 
			
		||||
    '\t</div>\n' +
 | 
			
		||||
    '</div>';
 | 
			
		||||
 | 
			
		||||
jQuery(function ($) {
 | 
			
		||||
    $.validator.addMethod('mac', function (value, element) {
 | 
			
		||||
        return this.optional(element) || /^([0-9A-F]{2}[:]){5}([0-9A-F]{2})$/.test(value);
 | 
			
		||||
    }, 'Please enter a valid MAC Address.(uppercase letters and numbers, : only) ex: 00:AB:12:EF:DD:AA');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
jQuery.each(["put", "delete"], function (i, method) {
 | 
			
		||||
    jQuery[method] = function (url, data, callback, type) {
 | 
			
		||||
        if (jQuery.isFunction(data)) {
 | 
			
		||||
            type = type || callback;
 | 
			
		||||
            callback = data;
 | 
			
		||||
            data = undefined;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return jQuery.ajax({
 | 
			
		||||
            url: url,
 | 
			
		||||
            type: method,
 | 
			
		||||
            dataType: type,
 | 
			
		||||
            data: data,
 | 
			
		||||
            success: callback,
 | 
			
		||||
            contentType: 'application/json'
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
jQuery(function ($) {
 | 
			
		||||
    let newHostHtml = '<div class="col-sm-2 offset-md-4" style=" text-align: right;"><button style="" id="btn_new_wake_on_lan_host" type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" data-target="#modal_wake_on_lan_host"><i class="nav-icon fas fa-plus"></i> New Host</button></div>';
 | 
			
		||||
    $('h1').parents(".row").append(newHostHtml);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
jQuery(function ($) {
 | 
			
		||||
    $('.btn-outline-success').click(function () {
 | 
			
		||||
        const $this = $(this);
 | 
			
		||||
        $.put(base_url + 'wake_on_lan_host/' + $this.data('mac-address'), function (result) {
 | 
			
		||||
            $this.parents('.info-box').find('.latest-used').text(prettyDateTime(result));
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
jQuery(function ($) {
 | 
			
		||||
    let $modal_remove_wake_on_lan_host = $('#modal_remove_wake_on_lan_host');
 | 
			
		||||
    let $remove_client_confirm = $('#remove_wake_on_host_confirm');
 | 
			
		||||
 | 
			
		||||
    $modal_remove_wake_on_lan_host.on('show.bs.modal', function (event) {
 | 
			
		||||
        const $btn = $(event.relatedTarget);
 | 
			
		||||
        const $modal = $(this);
 | 
			
		||||
 | 
			
		||||
        const $editBtn = $btn.parents('.btn-group').find('.btn_modify_wake_on_lan_host');
 | 
			
		||||
        $modal.find('.modal-body').text("You are about to remove Wake On Lan Host " + $editBtn.data('name'));
 | 
			
		||||
        $remove_client_confirm.val($editBtn.data('mac-address'));
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    $remove_client_confirm.click(function () {
 | 
			
		||||
        const macAddress = $remove_client_confirm.val().replaceAll(":", "-");
 | 
			
		||||
        $.delete(base_url + 'wake_on_lan_host/' + macAddress);
 | 
			
		||||
        $('#' + macAddress).remove();
 | 
			
		||||
 | 
			
		||||
        $modal_remove_wake_on_lan_host.modal('hide');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
jQuery(function ($) {
 | 
			
		||||
    $('.latest-used').each(function () {
 | 
			
		||||
        const $this = $(this);
 | 
			
		||||
        const timeText = $this.text().trim();
 | 
			
		||||
        try {
 | 
			
		||||
            if (timeText != "Unused") {
 | 
			
		||||
                $this.text(prettyDateTime(timeText));
 | 
			
		||||
            }
 | 
			
		||||
        } catch (ex) {
 | 
			
		||||
            console.log(timeText);
 | 
			
		||||
            throw ex;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
jQuery(function ($) {
 | 
			
		||||
    let $modal_wake_on_lan_host = $("#modal_wake_on_lan_host");
 | 
			
		||||
    let $name = $('#frm_wake_on_lan_host_name');
 | 
			
		||||
    let $macAddress = $('#frm_wake_on_lan_host_mac_address');
 | 
			
		||||
    let $oldMacAddress = $('#frm_wake_on_lan_host_old_mac_address');
 | 
			
		||||
    let $contentRow = $('.content .row');
 | 
			
		||||
    let $frm_wake_on_lan_host = $("#frm_wake_on_lan_host");
 | 
			
		||||
 | 
			
		||||
    // https://jqueryvalidation.org/
 | 
			
		||||
    let validator = $frm_wake_on_lan_host.validate({
 | 
			
		||||
        submitHandler: function () {
 | 
			
		||||
            let data = {
 | 
			
		||||
                name: $name.val(),
 | 
			
		||||
                mac_address: $macAddress.val().toUpperCase(),
 | 
			
		||||
                old_mac_address: $oldMacAddress.val().toUpperCase()
 | 
			
		||||
            };
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                url: base_url + 'wake_on_lan_host',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
                success: function (response) {
 | 
			
		||||
                    /** @type {string} */
 | 
			
		||||
                    let oldMacAddress = $oldMacAddress.val().toUpperCase();
 | 
			
		||||
 | 
			
		||||
                    if (oldMacAddress != '') {
 | 
			
		||||
                        let macAddress = response.MacAddress;
 | 
			
		||||
                        let name = response.Name;
 | 
			
		||||
 | 
			
		||||
                        let $container = $('#' + oldMacAddress.replaceAll(":", "-"));
 | 
			
		||||
                        if (macAddress != oldMacAddress) {
 | 
			
		||||
                            $container.attr('id', macAddress.replaceAll(":", "-"));
 | 
			
		||||
                            $container.find('.mac-address').text(macAddress);
 | 
			
		||||
                            $container.find('[data-mac-address]').data('mac-address', macAddress);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        $container.find('.name').text(name);
 | 
			
		||||
                        $container.find('[data-name]').data('name', name);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        const $template = $(
 | 
			
		||||
                            wake_on_lan_new_template
 | 
			
		||||
                                .replace(/{{ .Id }}/g, response.MacAddress.replaceAll(":", "-").toUpperCase())
 | 
			
		||||
                                .replace(/{{ .MacAddress }}/g, response.MacAddress.toUpperCase())
 | 
			
		||||
                                .replace(/{{ .Name }}/g, response.Name)
 | 
			
		||||
                        );
 | 
			
		||||
 | 
			
		||||
                        $contentRow.append($template);
 | 
			
		||||
                    }
 | 
			
		||||
                    $modal_wake_on_lan_host.modal('hide');
 | 
			
		||||
                    toastr.success('Wake on Lan Host Save successfully');
 | 
			
		||||
                },
 | 
			
		||||
                error: function (jqXHR, exception) {
 | 
			
		||||
                    const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                    toastr.error(responseJson['message']);
 | 
			
		||||
 | 
			
		||||
                    if (typeof (console) != 'undefined')
 | 
			
		||||
                        console.log(exception);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        },
 | 
			
		||||
        rules: {
 | 
			
		||||
            name: {
 | 
			
		||||
                required: true,
 | 
			
		||||
            },
 | 
			
		||||
            mac_address: {
 | 
			
		||||
                required: true,
 | 
			
		||||
                mac: true,
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        messages: {
 | 
			
		||||
            name: {
 | 
			
		||||
                required: "Please enter a name"
 | 
			
		||||
            },
 | 
			
		||||
            mac_address: {
 | 
			
		||||
                required: "Please enter a Mac Address"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        errorElement: 'span',
 | 
			
		||||
        errorPlacement: function (error, element) {
 | 
			
		||||
            error.addClass('invalid-feedback');
 | 
			
		||||
            element.closest('.form-group').append(error);
 | 
			
		||||
        },
 | 
			
		||||
        highlight: function (element) {
 | 
			
		||||
            $(element).addClass('is-invalid');
 | 
			
		||||
        },
 | 
			
		||||
        unhighlight: function (element) {
 | 
			
		||||
            $(element).removeClass('is-invalid');
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $modal_wake_on_lan_host.on('show.bs.modal', function (e) {
 | 
			
		||||
        const $btn = $(e.relatedTarget);
 | 
			
		||||
        validator.resetForm();
 | 
			
		||||
        $macAddress.removeClass('is-invalid');
 | 
			
		||||
 | 
			
		||||
        $name.val($btn.data('name'));
 | 
			
		||||
        $macAddress.val($btn.data('mac-address'));
 | 
			
		||||
        $oldMacAddress.val($btn.data('mac-address'));
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +15,9 @@ services:
 | 
			
		|||
      - SESSION_SECRET
 | 
			
		||||
      - WGUI_USERNAME=alpha
 | 
			
		||||
      - WGUI_PASSWORD=this-unusual-password
 | 
			
		||||
      - WG_CONF_TEMPLATE
 | 
			
		||||
      - WGUI_MANAGE_START=false
 | 
			
		||||
      - WGUI_MANAGE_RESTART=false
 | 
			
		||||
    logging:
 | 
			
		||||
      driver: json-file
 | 
			
		||||
      options:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,100 @@
 | 
			
		|||
package emailer
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	mail "github.com/xhit/go-simple-mail/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type SmtpMail struct {
 | 
			
		||||
	hostname   string
 | 
			
		||||
	port       int
 | 
			
		||||
	username   string
 | 
			
		||||
	password   string
 | 
			
		||||
	smtpHelo   string
 | 
			
		||||
	authType   mail.AuthType
 | 
			
		||||
	encryption mail.Encryption
 | 
			
		||||
	noTLSCheck bool
 | 
			
		||||
	fromName   string
 | 
			
		||||
	from       string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func authType(authType string) mail.AuthType {
 | 
			
		||||
	switch strings.ToUpper(authType) {
 | 
			
		||||
	case "PLAIN":
 | 
			
		||||
		return mail.AuthPlain
 | 
			
		||||
	case "LOGIN":
 | 
			
		||||
		return mail.AuthLogin
 | 
			
		||||
	default:
 | 
			
		||||
		return mail.AuthNone
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func encryptionType(encryptionType string) mail.Encryption {
 | 
			
		||||
	switch strings.ToUpper(encryptionType) {
 | 
			
		||||
	case "NONE":
 | 
			
		||||
		return mail.EncryptionNone
 | 
			
		||||
	case "SSL":
 | 
			
		||||
		return mail.EncryptionSSL
 | 
			
		||||
	case "SSLTLS":
 | 
			
		||||
		return mail.EncryptionSSLTLS
 | 
			
		||||
	case "TLS":
 | 
			
		||||
		return mail.EncryptionTLS
 | 
			
		||||
	default:
 | 
			
		||||
		return mail.EncryptionSTARTTLS
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSmtpMail(hostname string, port int, username string, password string, SmtpHelo string, noTLSCheck bool, auth string, fromName, from string, encryption string) *SmtpMail {
 | 
			
		||||
	ans := SmtpMail{hostname: hostname, port: port, username: username, password: password, smtpHelo: SmtpHelo, noTLSCheck: noTLSCheck, fromName: fromName, from: from, authType: authType(auth), encryption: encryptionType(encryption)}
 | 
			
		||||
	return &ans
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addressField(address string, name string) string {
 | 
			
		||||
	if name == "" {
 | 
			
		||||
		return address
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s <%s>", name, address)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *SmtpMail) Send(toName string, to string, subject string, content string, attachments []Attachment) error {
 | 
			
		||||
	server := mail.NewSMTPClient()
 | 
			
		||||
 | 
			
		||||
	server.Host = o.hostname
 | 
			
		||||
	server.Port = o.port
 | 
			
		||||
	server.Authentication = o.authType
 | 
			
		||||
	server.Username = o.username
 | 
			
		||||
	server.Password = o.password
 | 
			
		||||
	server.Helo = o.smtpHelo
 | 
			
		||||
	server.Encryption = o.encryption
 | 
			
		||||
	server.KeepAlive = false
 | 
			
		||||
	server.ConnectTimeout = 10 * time.Second
 | 
			
		||||
	server.SendTimeout = 10 * time.Second
 | 
			
		||||
 | 
			
		||||
	if o.noTLSCheck {
 | 
			
		||||
		server.TLSConfig = &tls.Config{InsecureSkipVerify: true}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	smtpClient, err := server.Connect()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	email := mail.NewMSG()
 | 
			
		||||
	email.SetFrom(addressField(o.from, o.fromName)).
 | 
			
		||||
		AddTo(addressField(to, toName)).
 | 
			
		||||
		SetSubject(subject).
 | 
			
		||||
		SetBody(mail.TextHTML, content)
 | 
			
		||||
 | 
			
		||||
	for _, v := range attachments {
 | 
			
		||||
		email.Attach(&mail.File{Name: v.Name, Data: v.Data})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = email.Send(smtpClient)
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,30 @@
 | 
			
		|||
## Prerequisites
 | 
			
		||||
 | 
			
		||||
### Kernel Module
 | 
			
		||||
 | 
			
		||||
Depending on if the Wireguard kernel module is available on your system you have more or less choices which example to use.
 | 
			
		||||
 | 
			
		||||
You can check if the kernel modules are available via the following command:
 | 
			
		||||
```shell
 | 
			
		||||
modprobe wireguard
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If the command exits successfully and doesn't print an error the kernel modules are available.
 | 
			
		||||
If it does error, you either have to install them manually (or activate if deactivated) or use an userspace implementation.
 | 
			
		||||
For an example of an userspace implementation, see _borigtun_.
 | 
			
		||||
 | 
			
		||||
### Credentials
 | 
			
		||||
 | 
			
		||||
Username and password for all examples is `admin` by default.
 | 
			
		||||
For security reasons it's highly recommended to change them before the first startup.
 | 
			
		||||
 | 
			
		||||
## Examples
 | 
			
		||||
- **[system](system.yml)**
 | 
			
		||||
 | 
			
		||||
  If you have Wireguard already installed on your system and only want to run the UI in docker this might fit the most.
 | 
			
		||||
- **[linuxserver](linuxserver.yml)**
 | 
			
		||||
 | 
			
		||||
  If you have the Wireguard kernel modules installed (included in the mainline kernel since version 5.6) but want it running inside of docker, this might fit the most.
 | 
			
		||||
- **[boringtun](boringtun.yml)**
 | 
			
		||||
 | 
			
		||||
  If Wireguard kernel modules are not available, you can switch to an userspace implementation like [boringtun](https://github.com/cloudflare/boringtun).
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
version: "3"
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  boringtun:
 | 
			
		||||
    image: ghcr.io/ntkme/boringtun:edge
 | 
			
		||||
    command:
 | 
			
		||||
      - wg0
 | 
			
		||||
    container_name: boringtun
 | 
			
		||||
    # use the network of the 'wireguard-ui' service. this enables to show active clients in the status page
 | 
			
		||||
    network_mode: service:wireguard-ui
 | 
			
		||||
    cap_add:
 | 
			
		||||
      - NET_ADMIN
 | 
			
		||||
    volumes:
 | 
			
		||||
      - /dev/net/tun:/dev/net/tun
 | 
			
		||||
      - ./config:/etc/wireguard
 | 
			
		||||
 | 
			
		||||
  wireguard-ui:
 | 
			
		||||
    image: ngoduykhanh/wireguard-ui:latest
 | 
			
		||||
    container_name: wireguard-ui
 | 
			
		||||
    cap_add:
 | 
			
		||||
      - NET_ADMIN
 | 
			
		||||
    environment:
 | 
			
		||||
      - SENDGRID_API_KEY
 | 
			
		||||
      - EMAIL_FROM_ADDRESS
 | 
			
		||||
      - EMAIL_FROM_NAME
 | 
			
		||||
      - SESSION_SECRET
 | 
			
		||||
      - WGUI_USERNAME=admin
 | 
			
		||||
      - WGUI_PASSWORD=admin
 | 
			
		||||
      - WG_CONF_TEMPLATE
 | 
			
		||||
      - WGUI_MANAGE_START=true
 | 
			
		||||
      - WGUI_MANAGE_RESTART=true
 | 
			
		||||
    logging:
 | 
			
		||||
      driver: json-file
 | 
			
		||||
      options:
 | 
			
		||||
        max-size: 50m
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./db:/app/db
 | 
			
		||||
      - ./config:/etc/wireguard
 | 
			
		||||
    ports:
 | 
			
		||||
      # port for wireguard-ui
 | 
			
		||||
      - "5000:5000"
 | 
			
		||||
      # port of the wireguard server. this must be set here as the `boringtun` container joins the network of this container and hasn't its own network over which it could publish the ports
 | 
			
		||||
      - "51820:51820/udp"
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
version: "3"
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  wireguard:
 | 
			
		||||
    image: linuxserver/wireguard:latest
 | 
			
		||||
    container_name: wireguard
 | 
			
		||||
    cap_add:
 | 
			
		||||
      - NET_ADMIN
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./config:/config
 | 
			
		||||
    ports:
 | 
			
		||||
      # port for wireguard-ui. this must be set here as the `wireguard-ui` container joins the network of this container and hasn't its own network over which it could publish the ports
 | 
			
		||||
      - "5000:5000"
 | 
			
		||||
      # port of the wireguard server
 | 
			
		||||
      - "51820:51820/udp"
 | 
			
		||||
 | 
			
		||||
  wireguard-ui:
 | 
			
		||||
    image: ngoduykhanh/wireguard-ui:latest
 | 
			
		||||
    container_name: wireguard-ui
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - wireguard
 | 
			
		||||
    cap_add:
 | 
			
		||||
      - NET_ADMIN
 | 
			
		||||
    # use the network of the 'wireguard' service. this enables to show active clients in the status page
 | 
			
		||||
    network_mode: service:wireguard
 | 
			
		||||
    environment:
 | 
			
		||||
      - SENDGRID_API_KEY
 | 
			
		||||
      - EMAIL_FROM_ADDRESS
 | 
			
		||||
      - EMAIL_FROM_NAME
 | 
			
		||||
      - SESSION_SECRET
 | 
			
		||||
      - WGUI_USERNAME=admin
 | 
			
		||||
      - WGUI_PASSWORD=admin
 | 
			
		||||
      - WG_CONF_TEMPLATE
 | 
			
		||||
      - WGUI_MANAGE_START=true
 | 
			
		||||
      - WGUI_MANAGE_RESTART=true
 | 
			
		||||
    logging:
 | 
			
		||||
      driver: json-file
 | 
			
		||||
      options:
 | 
			
		||||
        max-size: 50m
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./db:/app/db
 | 
			
		||||
      - ./config:/etc/wireguard
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
version: "3"
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  wireguard-ui:
 | 
			
		||||
    image: ngoduykhanh/wireguard-ui:latest
 | 
			
		||||
    container_name: wireguard-ui
 | 
			
		||||
    cap_add:
 | 
			
		||||
      - NET_ADMIN
 | 
			
		||||
    # required to show active clients. with this set, you don't need to expose the ui port (5000) anymore
 | 
			
		||||
    network_mode: host
 | 
			
		||||
    environment:
 | 
			
		||||
      - SENDGRID_API_KEY
 | 
			
		||||
      - EMAIL_FROM_ADDRESS
 | 
			
		||||
      - EMAIL_FROM_NAME
 | 
			
		||||
      - SESSION_SECRET
 | 
			
		||||
      - WGUI_USERNAME=admin
 | 
			
		||||
      - WGUI_PASSWORD=admin
 | 
			
		||||
      - WG_CONF_TEMPLATE
 | 
			
		||||
      - WGUI_MANAGE_START=false
 | 
			
		||||
      - WGUI_MANAGE_RESTART=false
 | 
			
		||||
    logging:
 | 
			
		||||
      driver: json-file
 | 
			
		||||
      options:
 | 
			
		||||
        max-size: 50m
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./db:/app/db
 | 
			
		||||
      - /etc/wireguard:/etc/wireguard
 | 
			
		||||
							
								
								
									
										62
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										62
									
								
								go.mod
								
								
								
								
							| 
						 | 
				
			
			@ -1,24 +1,52 @@
 | 
			
		|||
module github.com/ngoduykhanh/wireguard-ui
 | 
			
		||||
 | 
			
		||||
go 1.14
 | 
			
		||||
go 1.21
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/GeertJohan/go.rice v1.0.0
 | 
			
		||||
	github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd
 | 
			
		||||
	github.com/go-playground/universal-translator v0.17.0 // indirect
 | 
			
		||||
	github.com/gorilla/sessions v1.2.0
 | 
			
		||||
	github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 // indirect
 | 
			
		||||
	github.com/labstack/echo-contrib v0.9.0
 | 
			
		||||
	github.com/labstack/echo/v4 v4.1.16
 | 
			
		||||
	github.com/labstack/gommon v0.3.0
 | 
			
		||||
	github.com/leodido/go-urn v1.2.0 // indirect
 | 
			
		||||
	github.com/rs/xid v1.2.1
 | 
			
		||||
	github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba
 | 
			
		||||
	github.com/sendgrid/rest v2.6.4+incompatible // indirect
 | 
			
		||||
	github.com/sendgrid/sendgrid-go v3.10.0+incompatible
 | 
			
		||||
	github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086
 | 
			
		||||
	golang.zx2c4.com/wireguard v0.0.20200121 // indirect
 | 
			
		||||
	github.com/NicoNex/echotron/v3 v3.27.0
 | 
			
		||||
	github.com/glendc/go-external-ip v0.1.0
 | 
			
		||||
	github.com/gorilla/sessions v1.2.2
 | 
			
		||||
	github.com/labstack/echo-contrib v0.15.0
 | 
			
		||||
	github.com/labstack/echo/v4 v4.11.4
 | 
			
		||||
	github.com/labstack/gommon v0.4.2
 | 
			
		||||
	github.com/rs/xid v1.5.0
 | 
			
		||||
	github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d
 | 
			
		||||
	github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8
 | 
			
		||||
	github.com/sendgrid/sendgrid-go v3.14.0+incompatible
 | 
			
		||||
	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 | 
			
		||||
	github.com/xhit/go-simple-mail/v2 v2.16.0
 | 
			
		||||
	golang.org/x/crypto v0.17.0
 | 
			
		||||
	golang.org/x/mod v0.14.0
 | 
			
		||||
	//golang.zx2c4.com/wireguard v0.0.20200121 // indirect
 | 
			
		||||
	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
 | 
			
		||||
	gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
 | 
			
		||||
	gopkg.in/go-playground/validator.v9 v9.31.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/go-playground/locales v0.14.1 // indirect
 | 
			
		||||
	github.com/go-playground/universal-translator v0.18.1 // indirect
 | 
			
		||||
	github.com/go-test/deep v1.1.0 // indirect
 | 
			
		||||
	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
 | 
			
		||||
	github.com/google/go-cmp v0.6.0 // indirect
 | 
			
		||||
	github.com/gorilla/context v1.1.2 // indirect
 | 
			
		||||
	github.com/gorilla/securecookie v1.1.2 // indirect
 | 
			
		||||
	github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 // indirect
 | 
			
		||||
	github.com/josharian/native v1.1.0 // indirect
 | 
			
		||||
	github.com/leodido/go-urn v1.2.4 // indirect
 | 
			
		||||
	github.com/mattn/go-colorable v0.1.13 // indirect
 | 
			
		||||
	github.com/mattn/go-isatty v0.0.20 // indirect
 | 
			
		||||
	github.com/mdlayher/genetlink v1.3.2 // indirect
 | 
			
		||||
	github.com/mdlayher/netlink v1.7.2 // indirect
 | 
			
		||||
	github.com/mdlayher/socket v0.5.0 // indirect
 | 
			
		||||
	github.com/sendgrid/rest v2.6.9+incompatible // indirect
 | 
			
		||||
	github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
 | 
			
		||||
	github.com/valyala/bytebufferpool v1.0.0 // indirect
 | 
			
		||||
	github.com/valyala/fasttemplate v1.2.2 // indirect
 | 
			
		||||
	golang.org/x/net v0.19.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.5.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.15.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.14.0 // indirect
 | 
			
		||||
	golang.org/x/time v0.5.0 // indirect
 | 
			
		||||
	golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b // indirect
 | 
			
		||||
	gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										245
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										245
									
								
								go.sum
								
								
								
								
							| 
						 | 
				
			
			@ -1,93 +1,68 @@
 | 
			
		|||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
 | 
			
		||||
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
 | 
			
		||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
 | 
			
		||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
 | 
			
		||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
 | 
			
		||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 | 
			
		||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 | 
			
		||||
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
 | 
			
		||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 | 
			
		||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 | 
			
		||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 | 
			
		||||
github.com/casbin/casbin/v2 v2.0.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
 | 
			
		||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
 | 
			
		||||
github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY=
 | 
			
		||||
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
 | 
			
		||||
github.com/NicoNex/echotron/v3 v3.27.0 h1:iq4BLPO+Dz1JHjh2HPk0D0NldAZSYcAjaOicgYEhUzw=
 | 
			
		||||
github.com/NicoNex/echotron/v3 v3.27.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI=
 | 
			
		||||
github.com/coreos/bbolt v1.3.1-coreos.6.0.20180223184059-4f5275f4ebbf/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 | 
			
		||||
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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 | 
			
		||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 | 
			
		||||
github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd h1:1BzxHapafGJd/XlpMvocLeDBin2EKn90gXv2AQt5sfo=
 | 
			
		||||
github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd/go.mod h1:o9OoDQyE1WHvYVUH1FdFapy1/rCZHHq3O5wS4VA83ig=
 | 
			
		||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 | 
			
		||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 | 
			
		||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 | 
			
		||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
 | 
			
		||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
 | 
			
		||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
 | 
			
		||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
 | 
			
		||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 | 
			
		||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 | 
			
		||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
			
		||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
			
		||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
			
		||||
github.com/glendc/go-external-ip v0.1.0 h1:iX3xQ2Q26atAmLTbd++nUce2P5ht5P4uD4V7caSY/xg=
 | 
			
		||||
github.com/glendc/go-external-ip v0.1.0/go.mod h1:CNx312s2FLAJoWNdJWZ2Fpf5O4oLsMFwuYviHjS4uJE=
 | 
			
		||||
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.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 | 
			
		||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 | 
			
		||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
 | 
			
		||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
 | 
			
		||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 | 
			
		||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 | 
			
		||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 | 
			
		||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
			
		||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
			
		||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 | 
			
		||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 | 
			
		||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
 | 
			
		||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 | 
			
		||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
 | 
			
		||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
 | 
			
		||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
 | 
			
		||||
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
 | 
			
		||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
 | 
			
		||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 | 
			
		||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
			
		||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 | 
			
		||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 | 
			
		||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
 | 
			
		||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
 | 
			
		||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
 | 
			
		||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
 | 
			
		||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
 | 
			
		||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
 | 
			
		||||
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8=
 | 
			
		||||
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw=
 | 
			
		||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 | 
			
		||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
 | 
			
		||||
github.com/jessevdk/go-flags v0.0.0-20150816100521-1acbbaff2f34/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 | 
			
		||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
 | 
			
		||||
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/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
 | 
			
		||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
 | 
			
		||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
 | 
			
		||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
 | 
			
		||||
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
 | 
			
		||||
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
 | 
			
		||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
 | 
			
		||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
 | 
			
		||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 | 
			
		||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 | 
			
		||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 | 
			
		||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 | 
			
		||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 | 
			
		||||
github.com/labstack/echo-contrib v0.9.0 h1:hKBA2SnxdxR7sghH0J04zq/pImnKRmgvmQ6MvY9hug4=
 | 
			
		||||
github.com/labstack/echo-contrib v0.9.0/go.mod h1:TsFE5Vv0LRpZLoh4mMmaaAxzcTH+1CBFiUtVhwlegzU=
 | 
			
		||||
github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE=
 | 
			
		||||
github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o=
 | 
			
		||||
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
 | 
			
		||||
github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4=
 | 
			
		||||
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
 | 
			
		||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
 | 
			
		||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
 | 
			
		||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
			
		||||
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/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU=
 | 
			
		||||
github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4=
 | 
			
		||||
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
 | 
			
		||||
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
 | 
			
		||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
 | 
			
		||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
 | 
			
		||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
 | 
			
		||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 | 
			
		||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 | 
			
		||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 | 
			
		||||
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/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
 | 
			
		||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
 | 
			
		||||
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=
 | 
			
		||||
| 
						 | 
				
			
			@ -96,106 +71,78 @@ github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klX
 | 
			
		|||
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/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
 | 
			
		||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
 | 
			
		||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
 | 
			
		||||
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
 | 
			
		||||
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 | 
			
		||||
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/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 | 
			
		||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 | 
			
		||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
 | 
			
		||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 | 
			
		||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 | 
			
		||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 | 
			
		||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
 | 
			
		||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 | 
			
		||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 | 
			
		||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 | 
			
		||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
 | 
			
		||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 | 
			
		||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 | 
			
		||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
 | 
			
		||||
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
 | 
			
		||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
 | 
			
		||||
github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba h1:8QAc9wFAf2b/9cAXskm0wBylObZ0bTpRcaP7ThjLPVQ=
 | 
			
		||||
github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba/go.mod h1:W6zxGUBCXRR5QugSd/nFcFVmwoGnvpjiNY/JwT03Wew=
 | 
			
		||||
github.com/sendgrid/rest v2.6.4+incompatible h1:lq6gAQxLwVBf3mVyCCSHI6mgF+NfaJFJHjT0kl6SSo8=
 | 
			
		||||
github.com/sendgrid/rest v2.6.4+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
 | 
			
		||||
github.com/sendgrid/sendgrid-go v3.10.0+incompatible h1:aSYyurHxEZSDy7kxhvZ4fH0inNkEEmRssZNbAmETR2c=
 | 
			
		||||
github.com/sendgrid/sendgrid-go v3.10.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
 | 
			
		||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 | 
			
		||||
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs=
 | 
			
		||||
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
 | 
			
		||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
 | 
			
		||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 | 
			
		||||
github.com/sabhiram/go-colorize v0.0.0-20210403184538-366f55d711cf/go.mod h1:GvlEbMJBpbAXFn06UajbdBlGZ18iLvHyuIrgG//L8uk=
 | 
			
		||||
github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d h1:NDtoSmsxTpDYTqvUurn2ooAzDaYbJSB9/tOhLzaewgo=
 | 
			
		||||
github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d/go.mod h1:SVPBBd492Gk7Cq5lPd6OAYtIGk2r1FsyH8KT3IB8h7c=
 | 
			
		||||
github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8 h1:hlNRl87eAZhh2QMJVShuXHL6OOd0ObZM0JozDIruNeM=
 | 
			
		||||
github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8/go.mod h1:W6zxGUBCXRR5QugSd/nFcFVmwoGnvpjiNY/JwT03Wew=
 | 
			
		||||
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
 | 
			
		||||
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
 | 
			
		||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
 | 
			
		||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
 | 
			
		||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 | 
			
		||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
 | 
			
		||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 | 
			
		||||
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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 | 
			
		||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 | 
			
		||||
github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
 | 
			
		||||
github.com/uber/jaeger-client-go v2.19.1-0.20191002155754-0be28c34dabf+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
 | 
			
		||||
github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
 | 
			
		||||
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 v0.0.0-20150929183540-2b15294402a8/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 | 
			
		||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 | 
			
		||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 | 
			
		||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 | 
			
		||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 | 
			
		||||
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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 | 
			
		||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 | 
			
		||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
 | 
			
		||||
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
 | 
			
		||||
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
 | 
			
		||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
			
		||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
 | 
			
		||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 | 
			
		||||
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
 | 
			
		||||
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e h1:8foAy0aoO5GkqCvAEJ4VC4P3zksTg4X4aJCDpZzmgQI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 | 
			
		||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
 | 
			
		||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 | 
			
		||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
 | 
			
		||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 | 
			
		||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
			
		||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
			
		||||
golang.org/x/net v0.0.0-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-20200226121028-0de0cce0169b/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-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-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
 | 
			
		||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 | 
			
		||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
 | 
			
		||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
 | 
			
		||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
 | 
			
		||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190609082536-301114b31cce/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/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-20200223170610-d5e6a3e2c0ae/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=
 | 
			
		||||
| 
						 | 
				
			
			@ -207,33 +154,35 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
 | 
			
		|||
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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6 h1:cdsMqa2nXzqlgs183pHxtvoVwU7CyzaCTAUOg94af4c=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
 | 
			
		||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 | 
			
		||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 | 
			
		||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
			
		||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
 | 
			
		||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 | 
			
		||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 | 
			
		||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 | 
			
		||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 | 
			
		||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 | 
			
		||||
golang.org/x/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 h1:XDLXhn7ryprJVo+Lpkiib6CIuXE2031GDwtfEm7vLjI=
 | 
			
		||||
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
 | 
			
		||||
golang.zx2c4.com/wireguard v0.0.20200121 h1:vcswa5Q6f+sylDfjqyrVNNrjsFUUbPsgAQTBCAg/Qf8=
 | 
			
		||||
golang.zx2c4.com/wireguard v0.0.20200121/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
 | 
			
		||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
 | 
			
		||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
 | 
			
		||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
 | 
			
		||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
 | 
			
		||||
gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M=
 | 
			
		||||
gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
 | 
			
		||||
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.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ContentTypeJson checks that the requests have the Content-Type header set to "application/json".
 | 
			
		||||
// This helps against CSRF attacks.
 | 
			
		||||
func ContentTypeJson(next echo.HandlerFunc) echo.HandlerFunc {
 | 
			
		||||
	return func(c echo.Context) error {
 | 
			
		||||
		contentType := c.Request().Header.Get("Content-Type")
 | 
			
		||||
		if contentType != "application/json" {
 | 
			
		||||
			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Only JSON allowed"})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return next(c)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -0,0 +1,172 @@
 | 
			
		|||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
	"github.com/labstack/gommon/log"
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/model"
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/store"
 | 
			
		||||
	"github.com/sabhiram/go-wol/wol"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type WakeOnLanHostSavePayload struct {
 | 
			
		||||
	Name          string `json:"name"`
 | 
			
		||||
	MacAddress    string `json:"mac_address"`
 | 
			
		||||
	OldMacAddress string `json:"old_mac_address"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func createError(c echo.Context, err error, msg string) error {
 | 
			
		||||
	log.Error(msg, err)
 | 
			
		||||
	return c.JSON(
 | 
			
		||||
		http.StatusInternalServerError,
 | 
			
		||||
		jsonHTTPResponse{
 | 
			
		||||
			false,
 | 
			
		||||
			msg})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetWakeOnLanHosts(db store.IStore) echo.HandlerFunc {
 | 
			
		||||
	return func(c echo.Context) error {
 | 
			
		||||
		var err error
 | 
			
		||||
 | 
			
		||||
		hosts, err := db.GetWakeOnLanHosts()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return createError(c, err, fmt.Sprintf("wake_on_lan_hosts database error: %s", err))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = c.Render(http.StatusOK, "wake_on_lan_hosts.html", map[string]interface{}{
 | 
			
		||||
			"baseData": model.BaseData{Active: "wake_on_lan_hosts", CurrentUser: currentUser(c), Admin: isAdmin(c)},
 | 
			
		||||
			"hosts":    hosts,
 | 
			
		||||
			"error":    "",
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return createError(c, err, fmt.Sprintf("wake_on_lan_hosts.html render error: %s", err))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SaveWakeOnLanHost(db store.IStore) echo.HandlerFunc {
 | 
			
		||||
	return func(c echo.Context) error {
 | 
			
		||||
		var payload WakeOnLanHostSavePayload
 | 
			
		||||
		err := c.Bind(&payload)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Wake On Host Save Payload Bind Error: ", err)
 | 
			
		||||
			return c.JSON(http.StatusInternalServerError, payload)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var host = model.WakeOnLanHost{
 | 
			
		||||
			MacAddress: payload.MacAddress,
 | 
			
		||||
			Name:       payload.Name,
 | 
			
		||||
		}
 | 
			
		||||
		if len(payload.OldMacAddress) != 0 { // Edit
 | 
			
		||||
			if payload.OldMacAddress != payload.MacAddress { // modified mac address
 | 
			
		||||
				oldHost, err := db.GetWakeOnLanHost(payload.OldMacAddress)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return createError(c, err, fmt.Sprintf("Wake On Host Update Err: %s", err))
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if payload.OldMacAddress != payload.MacAddress {
 | 
			
		||||
					existHost, _ := db.GetWakeOnLanHost(payload.MacAddress)
 | 
			
		||||
					if existHost != nil {
 | 
			
		||||
						return createError(c, nil, "Mac Address already exists.")
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				err = db.DeleteWakeOnHostLanHost(payload.OldMacAddress)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return createError(c, err, fmt.Sprintf("Wake On Host Update Err: %s", err))
 | 
			
		||||
				}
 | 
			
		||||
				host.LatestUsed = oldHost.LatestUsed
 | 
			
		||||
			}
 | 
			
		||||
			err = db.SaveWakeOnLanHost(host)
 | 
			
		||||
		} else { // new
 | 
			
		||||
			existHost, _ := db.GetWakeOnLanHost(payload.MacAddress)
 | 
			
		||||
			if existHost != nil {
 | 
			
		||||
				return createError(c, nil, "Mac Address already exists.")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = db.SaveWakeOnLanHost(host)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return createError(c, err, fmt.Sprintf("Wake On Host Save Error: %s", err))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return c.JSON(http.StatusOK, host)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DeleteWakeOnHost(db store.IStore) echo.HandlerFunc {
 | 
			
		||||
	return func(c echo.Context) error {
 | 
			
		||||
		var macAddress = c.Param("mac_address")
 | 
			
		||||
		var host, err = db.GetWakeOnLanHost(macAddress)
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Wake On Host Delete Error: ", err)
 | 
			
		||||
			return createError(c, err, fmt.Sprintf("Wake On Host Delete Error: %s", macAddress))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = db.DeleteWakeOnHost(*host)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return createError(c, err, fmt.Sprintf("Wake On Host Delete Error: %s", macAddress))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return c.JSON(http.StatusOK, nil)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func WakeOnHost(db store.IStore) echo.HandlerFunc {
 | 
			
		||||
	return func(c echo.Context) error {
 | 
			
		||||
		macAddress := c.Param("mac_address")
 | 
			
		||||
		host, err := db.GetWakeOnLanHost(macAddress)
 | 
			
		||||
 | 
			
		||||
		now := time.Now().UTC()
 | 
			
		||||
		host.LatestUsed = &now
 | 
			
		||||
		err = db.SaveWakeOnLanHost(*host)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return createError(c, err, fmt.Sprintf("Latest Used Update Error: %s", macAddress))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		magicPacket, err := wol.New(macAddress)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return createError(c, err, fmt.Sprintf("Magic Packet Create Error: %s", macAddress))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		bytes, err := magicPacket.Marshal()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return createError(c, err, fmt.Sprintf("Magic Packet Bytestream Error: %s", macAddress))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		udpAddr, err := net.ResolveUDPAddr("udp", "255.255.255.255:0")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return createError(c, err, fmt.Sprintf("ResolveUDPAddr Error: %s", macAddress))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Grab a UDP connection to send our packet of bytes.
 | 
			
		||||
		conn, err := net.DialUDP("udp", nil, udpAddr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		defer func(conn *net.UDPConn) {
 | 
			
		||||
			err := conn.Close()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Error(err)
 | 
			
		||||
			}
 | 
			
		||||
		}(conn)
 | 
			
		||||
 | 
			
		||||
		n, err := conn.Write(bytes)
 | 
			
		||||
		if err == nil && n != 102 {
 | 
			
		||||
			return createError(c, nil, fmt.Sprintf("magic packet sent was %d bytes (expected 102 bytes sent)", n))
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return createError(c, err, fmt.Sprintf("Network Send Error: %s", macAddress))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return c.JSON(http.StatusOK, host.LatestUsed)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,7 +3,9 @@ package handler
 | 
			
		|||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gorilla/sessions"
 | 
			
		||||
	"github.com/labstack/echo-contrib/session"
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/util"
 | 
			
		||||
| 
						 | 
				
			
			@ -14,15 +16,33 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc {
 | 
			
		|||
		if !isValidSession(c) {
 | 
			
		||||
			nextURL := c.Request().URL
 | 
			
		||||
			if nextURL != nil && c.Request().Method == http.MethodGet {
 | 
			
		||||
				return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("/login?next=%s", c.Request().URL))
 | 
			
		||||
				return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath+"/login?next=%s", c.Request().URL))
 | 
			
		||||
			} else {
 | 
			
		||||
				return c.Redirect(http.StatusTemporaryRedirect, "/login")
 | 
			
		||||
				return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login")
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return next(c)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RefreshSession must only be used after ValidSession middleware
 | 
			
		||||
// RefreshSession checks if the session is eligible for the refresh, but doesn't check if it's fully valid
 | 
			
		||||
func RefreshSession(next echo.HandlerFunc) echo.HandlerFunc {
 | 
			
		||||
	return func(c echo.Context) error {
 | 
			
		||||
		doRefreshSession(c)
 | 
			
		||||
		return next(c)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NeedsAdmin(next echo.HandlerFunc) echo.HandlerFunc {
 | 
			
		||||
	return func(c echo.Context) error {
 | 
			
		||||
		if !isAdmin(c) {
 | 
			
		||||
			return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/")
 | 
			
		||||
		}
 | 
			
		||||
		return next(c)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isValidSession(c echo.Context) bool {
 | 
			
		||||
	if util.DisableLogin {
 | 
			
		||||
		return true
 | 
			
		||||
| 
						 | 
				
			
			@ -32,9 +52,146 @@ func isValidSession(c echo.Context) bool {
 | 
			
		|||
	if err != nil || sess.Values["session_token"] != cookie.Value {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check time bounds
 | 
			
		||||
	createdAt := getCreatedAt(sess)
 | 
			
		||||
	updatedAt := getUpdatedAt(sess)
 | 
			
		||||
	maxAge := getMaxAge(sess)
 | 
			
		||||
	// Temporary session is considered valid within 24h if browser is not closed before
 | 
			
		||||
	// This value is not saved and is used as virtual expiration
 | 
			
		||||
	if maxAge == 0 {
 | 
			
		||||
		maxAge = 86400
 | 
			
		||||
	}
 | 
			
		||||
	expiration := updatedAt + int64(maxAge)
 | 
			
		||||
	now := time.Now().UTC().Unix()
 | 
			
		||||
	if updatedAt > now || expiration < now || createdAt+util.SessionMaxDuration < now {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check if user still exists and unchanged
 | 
			
		||||
	username := fmt.Sprintf("%s", sess.Values["username"])
 | 
			
		||||
	userHash := getUserHash(sess)
 | 
			
		||||
	if uHash, ok := util.DBUsersToCRC32[username]; !ok || userHash != uHash {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Refreshes a "remember me" session when the user visits web pages (not API)
 | 
			
		||||
// Session must be valid before calling this function
 | 
			
		||||
// Refresh is performed at most once per 24h
 | 
			
		||||
func doRefreshSession(c echo.Context) {
 | 
			
		||||
	if util.DisableLogin {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sess, _ := session.Get("session", c)
 | 
			
		||||
	maxAge := getMaxAge(sess)
 | 
			
		||||
	if maxAge <= 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	oldCookie, err := c.Cookie("session_token")
 | 
			
		||||
	if err != nil || sess.Values["session_token"] != oldCookie.Value {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Refresh no sooner than 24h
 | 
			
		||||
	createdAt := getCreatedAt(sess)
 | 
			
		||||
	updatedAt := getUpdatedAt(sess)
 | 
			
		||||
	expiration := updatedAt + int64(getMaxAge(sess))
 | 
			
		||||
	now := time.Now().UTC().Unix()
 | 
			
		||||
	if updatedAt > now || expiration < now || now-updatedAt < 86_400 || createdAt+util.SessionMaxDuration < now {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cookiePath := util.GetCookiePath()
 | 
			
		||||
 | 
			
		||||
	sess.Values["updated_at"] = now
 | 
			
		||||
	sess.Options = &sessions.Options{
 | 
			
		||||
		Path:     cookiePath,
 | 
			
		||||
		MaxAge:   maxAge,
 | 
			
		||||
		HttpOnly: true,
 | 
			
		||||
		SameSite: http.SameSiteLaxMode,
 | 
			
		||||
	}
 | 
			
		||||
	sess.Save(c.Request(), c.Response())
 | 
			
		||||
 | 
			
		||||
	cookie := new(http.Cookie)
 | 
			
		||||
	cookie.Name = "session_token"
 | 
			
		||||
	cookie.Path = cookiePath
 | 
			
		||||
	cookie.Value = oldCookie.Value
 | 
			
		||||
	cookie.MaxAge = maxAge
 | 
			
		||||
	cookie.HttpOnly = true
 | 
			
		||||
	cookie.SameSite = http.SameSiteLaxMode
 | 
			
		||||
	c.SetCookie(cookie)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get time in seconds this session is valid without updating
 | 
			
		||||
func getMaxAge(sess *sessions.Session) int {
 | 
			
		||||
	if util.DisableLogin {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	maxAge := sess.Values["max_age"]
 | 
			
		||||
 | 
			
		||||
	switch typedMaxAge := maxAge.(type) {
 | 
			
		||||
	case int:
 | 
			
		||||
		return typedMaxAge
 | 
			
		||||
	default:
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get a timestamp in seconds of the time the session was created
 | 
			
		||||
func getCreatedAt(sess *sessions.Session) int64 {
 | 
			
		||||
	if util.DisableLogin {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	createdAt := sess.Values["created_at"]
 | 
			
		||||
 | 
			
		||||
	switch typedCreatedAt := createdAt.(type) {
 | 
			
		||||
	case int64:
 | 
			
		||||
		return typedCreatedAt
 | 
			
		||||
	default:
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get a timestamp in seconds of the last session update
 | 
			
		||||
func getUpdatedAt(sess *sessions.Session) int64 {
 | 
			
		||||
	if util.DisableLogin {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lastUpdate := sess.Values["updated_at"]
 | 
			
		||||
 | 
			
		||||
	switch typedLastUpdate := lastUpdate.(type) {
 | 
			
		||||
	case int64:
 | 
			
		||||
		return typedLastUpdate
 | 
			
		||||
	default:
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get CRC32 of a user at the moment of log in
 | 
			
		||||
// Any changes to user will result in logout of other (not updated) sessions
 | 
			
		||||
func getUserHash(sess *sessions.Session) uint32 {
 | 
			
		||||
	if util.DisableLogin {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userHash := sess.Values["user_hash"]
 | 
			
		||||
 | 
			
		||||
	switch typedUserHash := userHash.(type) {
 | 
			
		||||
	case uint32:
 | 
			
		||||
		return typedUserHash
 | 
			
		||||
	default:
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// currentUser to get username of logged in user
 | 
			
		||||
func currentUser(c echo.Context) string {
 | 
			
		||||
	if util.DisableLogin {
 | 
			
		||||
| 
						 | 
				
			
			@ -46,10 +203,47 @@ func currentUser(c echo.Context) string {
 | 
			
		|||
	return username
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// isAdmin to get user type: admin or manager
 | 
			
		||||
func isAdmin(c echo.Context) bool {
 | 
			
		||||
	if util.DisableLogin {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sess, _ := session.Get("session", c)
 | 
			
		||||
	admin := fmt.Sprintf("%t", sess.Values["admin"])
 | 
			
		||||
	return admin == "true"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setUser(c echo.Context, username string, admin bool, userCRC32 uint32) {
 | 
			
		||||
	sess, _ := session.Get("session", c)
 | 
			
		||||
	sess.Values["username"] = username
 | 
			
		||||
	sess.Values["user_hash"] = userCRC32
 | 
			
		||||
	sess.Values["admin"] = admin
 | 
			
		||||
	sess.Save(c.Request(), c.Response())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// clearSession to remove current session
 | 
			
		||||
func clearSession(c echo.Context) {
 | 
			
		||||
	sess, _ := session.Get("session", c)
 | 
			
		||||
	sess.Values["username"] = ""
 | 
			
		||||
	sess.Values["user_hash"] = 0
 | 
			
		||||
	sess.Values["admin"] = false
 | 
			
		||||
	sess.Values["session_token"] = ""
 | 
			
		||||
	sess.Values["max_age"] = -1
 | 
			
		||||
	sess.Options.MaxAge = -1
 | 
			
		||||
	sess.Save(c.Request(), c.Response())
 | 
			
		||||
 | 
			
		||||
	cookiePath := util.GetCookiePath()
 | 
			
		||||
 | 
			
		||||
	cookie, err := c.Cookie("session_token")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		cookie = new(http.Cookie)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cookie.Name = "session_token"
 | 
			
		||||
	cookie.Path = cookiePath
 | 
			
		||||
	cookie.MaxAge = -1
 | 
			
		||||
	cookie.HttpOnly = true
 | 
			
		||||
	cookie.SameSite = http.SameSiteLaxMode
 | 
			
		||||
	c.SetCookie(cookie)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
# extract wg config file path, or use default
 | 
			
		||||
conf="$(jq -r .config_file_path db/server/global_settings.json || echo /etc/wireguard/wg0.conf)"
 | 
			
		||||
 | 
			
		||||
# manage wireguard stop/start with the container
 | 
			
		||||
case $WGUI_MANAGE_START in (1|t|T|true|True|TRUE)
 | 
			
		||||
    wg-quick up "$conf"
 | 
			
		||||
    trap 'wg-quick down "$conf"' SIGTERM # catches container stop
 | 
			
		||||
esac
 | 
			
		||||
 | 
			
		||||
# manage wireguard restarts
 | 
			
		||||
case $WGUI_MANAGE_RESTART in (1|t|T|true|True|TRUE)
 | 
			
		||||
    [[ -f $conf ]] || touch "$conf" # inotifyd needs file to exist
 | 
			
		||||
    inotifyd - "$conf":w | while read -r event file; do
 | 
			
		||||
        wg-quick down "$file"
 | 
			
		||||
        wg-quick up "$file"
 | 
			
		||||
    done &
 | 
			
		||||
esac
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
./wg-ui &
 | 
			
		||||
wait $!
 | 
			
		||||
							
								
								
									
										328
									
								
								main.go
								
								
								
								
							
							
						
						
									
										328
									
								
								main.go
								
								
								
								
							| 
						 | 
				
			
			@ -1,13 +1,22 @@
 | 
			
		|||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/sha512"
 | 
			
		||||
	"embed"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	rice "github.com/GeertJohan/go.rice"
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
	"github.com/labstack/gommon/log"
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/store"
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/telegram"
 | 
			
		||||
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/emailer"
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/handler"
 | 
			
		||||
| 
						 | 
				
			
			@ -23,12 +32,27 @@ var (
 | 
			
		|||
	gitRef     = "N/A"
 | 
			
		||||
	buildTime  = fmt.Sprintf(time.Now().UTC().Format("01-02-2006 15:04:05"))
 | 
			
		||||
	// configuration variables
 | 
			
		||||
	flagDisableLogin   bool   = false
 | 
			
		||||
	flagBindAddress    string = "0.0.0.0:5000"
 | 
			
		||||
	flagSendgridApiKey string
 | 
			
		||||
	flagEmailFrom      string
 | 
			
		||||
	flagEmailFromName  string = "WireGuard UI"
 | 
			
		||||
	flagSessionSecret  string
 | 
			
		||||
	flagDisableLogin             = false
 | 
			
		||||
	flagBindAddress              = "0.0.0.0:5000"
 | 
			
		||||
	flagSmtpHostname             = "127.0.0.1"
 | 
			
		||||
	flagSmtpPort                 = 25
 | 
			
		||||
	flagSmtpUsername             string
 | 
			
		||||
	flagSmtpPassword             string
 | 
			
		||||
	flagSmtpAuthType             = "NONE"
 | 
			
		||||
	flagSmtpNoTLSCheck           = false
 | 
			
		||||
	flagSmtpEncryption           = "STARTTLS"
 | 
			
		||||
	flagSmtpHelo                 = "localhost"
 | 
			
		||||
	flagSendgridApiKey           string
 | 
			
		||||
	flagEmailFrom                string
 | 
			
		||||
	flagEmailFromName            = "WireGuard UI"
 | 
			
		||||
	flagTelegramToken            string
 | 
			
		||||
	flagTelegramAllowConfRequest = false
 | 
			
		||||
	flagTelegramFloodWait        = 60
 | 
			
		||||
	flagSessionSecret            = util.RandomString(32)
 | 
			
		||||
	flagSessionMaxDuration       = 90
 | 
			
		||||
	flagWgConfTemplate           string
 | 
			
		||||
	flagBasePath                 string
 | 
			
		||||
	flagSubnetRanges             string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
| 
						 | 
				
			
			@ -40,39 +64,112 @@ const (
 | 
			
		|||
`
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
// embed the "templates" directory
 | 
			
		||||
//
 | 
			
		||||
//go:embed templates/*
 | 
			
		||||
var embeddedTemplates embed.FS
 | 
			
		||||
 | 
			
		||||
// embed the "assets" directory
 | 
			
		||||
//
 | 
			
		||||
//go:embed assets/*
 | 
			
		||||
var embeddedAssets embed.FS
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	// command-line flags and env variables
 | 
			
		||||
	flag.BoolVar(&flagDisableLogin, "disable-login", util.LookupEnvOrBool("DISABLE_LOGIN", flagDisableLogin), "Disable authentication on the app. This is potentially dangerous.")
 | 
			
		||||
	flag.StringVar(&flagBindAddress, "bind-address", util.LookupEnvOrString("BIND_ADDRESS", flagBindAddress), "Address:Port to which the app will be bound.")
 | 
			
		||||
	flag.StringVar(&flagSendgridApiKey, "sendgrid-api-key", util.LookupEnvOrString("SENDGRID_API_KEY", flagSendgridApiKey), "Your sendgrid api key.")
 | 
			
		||||
	flag.StringVar(&flagSmtpHostname, "smtp-hostname", util.LookupEnvOrString("SMTP_HOSTNAME", flagSmtpHostname), "SMTP Hostname")
 | 
			
		||||
	flag.IntVar(&flagSmtpPort, "smtp-port", util.LookupEnvOrInt("SMTP_PORT", flagSmtpPort), "SMTP Port")
 | 
			
		||||
	flag.StringVar(&flagSmtpHelo, "smtp-helo", util.LookupEnvOrString("SMTP_HELO", flagSmtpHelo), "SMTP HELO Hostname")
 | 
			
		||||
	flag.StringVar(&flagSmtpUsername, "smtp-username", util.LookupEnvOrString("SMTP_USERNAME", flagSmtpUsername), "SMTP Username")
 | 
			
		||||
	flag.BoolVar(&flagSmtpNoTLSCheck, "smtp-no-tls-check", util.LookupEnvOrBool("SMTP_NO_TLS_CHECK", flagSmtpNoTLSCheck), "Disable TLS verification for SMTP. This is potentially dangerous.")
 | 
			
		||||
	flag.StringVar(&flagSmtpEncryption, "smtp-encryption", util.LookupEnvOrString("SMTP_ENCRYPTION", flagSmtpEncryption), "SMTP Encryption : NONE, SSL, SSLTLS, TLS or STARTTLS (by default)")
 | 
			
		||||
	flag.StringVar(&flagSmtpAuthType, "smtp-auth-type", util.LookupEnvOrString("SMTP_AUTH_TYPE", flagSmtpAuthType), "SMTP Auth Type : PLAIN, LOGIN or NONE.")
 | 
			
		||||
	flag.StringVar(&flagEmailFrom, "email-from", util.LookupEnvOrString("EMAIL_FROM_ADDRESS", flagEmailFrom), "'From' email address.")
 | 
			
		||||
	flag.StringVar(&flagEmailFromName, "email-from-name", util.LookupEnvOrString("EMAIL_FROM_NAME", flagEmailFromName), "'From' email name.")
 | 
			
		||||
	flag.StringVar(&flagSessionSecret, "session-secret", util.LookupEnvOrString("SESSION_SECRET", flagSessionSecret), "The key used to encrypt session cookies.")
 | 
			
		||||
	flag.StringVar(&flagTelegramToken, "telegram-token", util.LookupEnvOrString("TELEGRAM_TOKEN", flagTelegramToken), "Telegram bot token for distributing configs to clients.")
 | 
			
		||||
	flag.BoolVar(&flagTelegramAllowConfRequest, "telegram-allow-conf-request", util.LookupEnvOrBool("TELEGRAM_ALLOW_CONF_REQUEST", flagTelegramAllowConfRequest), "Allow users to get configs from the bot by sending a message.")
 | 
			
		||||
	flag.IntVar(&flagTelegramFloodWait, "telegram-flood-wait", util.LookupEnvOrInt("TELEGRAM_FLOOD_WAIT", flagTelegramFloodWait), "Time in minutes before the next conf request is processed.")
 | 
			
		||||
	flag.StringVar(&flagWgConfTemplate, "wg-conf-template", util.LookupEnvOrString("WG_CONF_TEMPLATE", flagWgConfTemplate), "Path to custom wg.conf template.")
 | 
			
		||||
	flag.StringVar(&flagBasePath, "base-path", util.LookupEnvOrString("BASE_PATH", flagBasePath), "The base path of the URL")
 | 
			
		||||
	flag.StringVar(&flagSubnetRanges, "subnet-ranges", util.LookupEnvOrString("SUBNET_RANGES", flagSubnetRanges), "IP ranges to choose from when assigning an IP for a client.")
 | 
			
		||||
	flag.IntVar(&flagSessionMaxDuration, "session-max-duration", util.LookupEnvOrInt("SESSION_MAX_DURATION", flagSessionMaxDuration), "Max time in days a remembered session is refreshed and valid.")
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		smtpPasswordLookup   = util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword)
 | 
			
		||||
		sendgridApiKeyLookup = util.LookupEnvOrString("SENDGRID_API_KEY", flagSendgridApiKey)
 | 
			
		||||
		sessionSecretLookup  = util.LookupEnvOrString("SESSION_SECRET", flagSessionSecret)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// check empty smtpPassword env var
 | 
			
		||||
	if smtpPasswordLookup != "" {
 | 
			
		||||
		flag.StringVar(&flagSmtpPassword, "smtp-password", smtpPasswordLookup, "SMTP Password")
 | 
			
		||||
	} else {
 | 
			
		||||
		flag.StringVar(&flagSmtpPassword, "smtp-password", util.LookupEnvOrFile("SMTP_PASSWORD_FILE", flagSmtpPassword), "SMTP Password File")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check empty sendgridApiKey env var
 | 
			
		||||
	if sendgridApiKeyLookup != "" {
 | 
			
		||||
		flag.StringVar(&flagSendgridApiKey, "sendgrid-api-key", sendgridApiKeyLookup, "Your sendgrid api key.")
 | 
			
		||||
	} else {
 | 
			
		||||
		flag.StringVar(&flagSendgridApiKey, "sendgrid-api-key", util.LookupEnvOrFile("SENDGRID_API_KEY_FILE", flagSendgridApiKey), "File containing your sendgrid api key.")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check empty sessionSecret env var
 | 
			
		||||
	if sessionSecretLookup != "" {
 | 
			
		||||
		flag.StringVar(&flagSessionSecret, "session-secret", sessionSecretLookup, "The key used to encrypt session cookies.")
 | 
			
		||||
	} else {
 | 
			
		||||
		flag.StringVar(&flagSessionSecret, "session-secret", util.LookupEnvOrFile("SESSION_SECRET_FILE", flagSessionSecret), "File containing the key used to encrypt session cookies.")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	// update runtime config
 | 
			
		||||
	util.DisableLogin = flagDisableLogin
 | 
			
		||||
	util.BindAddress = flagBindAddress
 | 
			
		||||
	util.SmtpHostname = flagSmtpHostname
 | 
			
		||||
	util.SmtpPort = flagSmtpPort
 | 
			
		||||
	util.SmtpHelo = flagSmtpHelo
 | 
			
		||||
	util.SmtpUsername = flagSmtpUsername
 | 
			
		||||
	util.SmtpPassword = flagSmtpPassword
 | 
			
		||||
	util.SmtpAuthType = flagSmtpAuthType
 | 
			
		||||
	util.SmtpNoTLSCheck = flagSmtpNoTLSCheck
 | 
			
		||||
	util.SmtpEncryption = flagSmtpEncryption
 | 
			
		||||
	util.SendgridApiKey = flagSendgridApiKey
 | 
			
		||||
	util.EmailFrom = flagEmailFrom
 | 
			
		||||
	util.EmailFromName = flagEmailFromName
 | 
			
		||||
	util.SessionSecret = []byte(flagSessionSecret)
 | 
			
		||||
	util.SessionSecret = sha512.Sum512([]byte(flagSessionSecret))
 | 
			
		||||
	util.SessionMaxDuration = int64(flagSessionMaxDuration) * 86_400 // Store in seconds
 | 
			
		||||
	util.WgConfTemplate = flagWgConfTemplate
 | 
			
		||||
	util.BasePath = util.ParseBasePath(flagBasePath)
 | 
			
		||||
	util.SubnetRanges = util.ParseSubnetRanges(flagSubnetRanges)
 | 
			
		||||
 | 
			
		||||
	// print app information
 | 
			
		||||
	fmt.Println("Wireguard UI")
 | 
			
		||||
	fmt.Println("App Version\t:", appVersion)
 | 
			
		||||
	fmt.Println("Git Commit\t:", gitCommit)
 | 
			
		||||
	fmt.Println("Git Ref\t\t:", gitRef)
 | 
			
		||||
	fmt.Println("Build Time\t:", buildTime)
 | 
			
		||||
	fmt.Println("Git Repo\t:", "https://github.com/ngoduykhanh/wireguard-ui")
 | 
			
		||||
	fmt.Println("Authentication\t:", !util.DisableLogin)
 | 
			
		||||
	fmt.Println("Bind address\t:", util.BindAddress)
 | 
			
		||||
	//fmt.Println("Sendgrid key\t:", util.SendgridApiKey)
 | 
			
		||||
	fmt.Println("Email from\t:", util.EmailFrom)
 | 
			
		||||
	fmt.Println("Email from name\t:", util.EmailFromName)
 | 
			
		||||
	//fmt.Println("Session secret\t:", util.SessionSecret)
 | 
			
		||||
	lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO"))
 | 
			
		||||
 | 
			
		||||
	telegram.Token = flagTelegramToken
 | 
			
		||||
	telegram.AllowConfRequest = flagTelegramAllowConfRequest
 | 
			
		||||
	telegram.FloodWait = flagTelegramFloodWait
 | 
			
		||||
	telegram.LogLevel = lvl
 | 
			
		||||
 | 
			
		||||
	// print only if log level is INFO or lower
 | 
			
		||||
	if lvl <= log.INFO {
 | 
			
		||||
		// print app information
 | 
			
		||||
		fmt.Println("Wireguard UI")
 | 
			
		||||
		fmt.Println("App Version\t:", appVersion)
 | 
			
		||||
		fmt.Println("Git Commit\t:", gitCommit)
 | 
			
		||||
		fmt.Println("Git Ref\t\t:", gitRef)
 | 
			
		||||
		fmt.Println("Build Time\t:", buildTime)
 | 
			
		||||
		fmt.Println("Git Repo\t:", "https://github.com/ngoduykhanh/wireguard-ui")
 | 
			
		||||
		fmt.Println("Authentication\t:", !util.DisableLogin)
 | 
			
		||||
		fmt.Println("Bind address\t:", util.BindAddress)
 | 
			
		||||
		//fmt.Println("Sendgrid key\t:", util.SendgridApiKey)
 | 
			
		||||
		fmt.Println("Email from\t:", util.EmailFrom)
 | 
			
		||||
		fmt.Println("Email from name\t:", util.EmailFromName)
 | 
			
		||||
		//fmt.Println("Session secret\t:", util.SessionSecret)
 | 
			
		||||
		fmt.Println("Custom wg.conf\t:", util.WgConfTemplate)
 | 
			
		||||
		fmt.Println("Base path\t:", util.BasePath+"/")
 | 
			
		||||
		fmt.Println("Subnet ranges\t:", util.GetSubnetRangesString())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
| 
						 | 
				
			
			@ -84,49 +181,160 @@ func main() {
 | 
			
		|||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	// set app extra data
 | 
			
		||||
	extraData := make(map[string]string)
 | 
			
		||||
	extraData := make(map[string]interface{})
 | 
			
		||||
	extraData["appVersion"] = appVersion
 | 
			
		||||
	extraData["gitCommit"] = gitCommit
 | 
			
		||||
	extraData["basePath"] = util.BasePath
 | 
			
		||||
	extraData["loginDisabled"] = flagDisableLogin
 | 
			
		||||
 | 
			
		||||
	// create rice box for embedded template
 | 
			
		||||
	tmplBox := rice.MustFindBox("templates")
 | 
			
		||||
	// strip the "templates/" prefix from the embedded directory so files can be read by their direct name (e.g.
 | 
			
		||||
	// "base.html" instead of "templates/base.html")
 | 
			
		||||
	tmplDir, _ := fs.Sub(fs.FS(embeddedTemplates), "templates")
 | 
			
		||||
 | 
			
		||||
	// rice file server for assets. "assets" is the folder where the files come from.
 | 
			
		||||
	assetHandler := http.FileServer(rice.MustFindBox("assets").HTTPBox())
 | 
			
		||||
	// create the wireguard config on start, if it doesn't exist
 | 
			
		||||
	initServerConfig(db, tmplDir)
 | 
			
		||||
 | 
			
		||||
	// register routes
 | 
			
		||||
	app := router.New(tmplBox, extraData, util.SessionSecret)
 | 
			
		||||
 | 
			
		||||
	app.GET("/", handler.WireGuardClients(db), handler.ValidSession)
 | 
			
		||||
 | 
			
		||||
	if !util.DisableLogin {
 | 
			
		||||
		app.GET("/login", handler.LoginPage())
 | 
			
		||||
		app.POST("/login", handler.Login(db))
 | 
			
		||||
	// Check if subnet ranges are valid for the server configuration
 | 
			
		||||
	// Remove any non-valid CIDRs
 | 
			
		||||
	if err := util.ValidateAndFixSubnetRanges(db); err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sendmail := emailer.NewSendgridApiMail(util.SendgridApiKey, util.EmailFromName, util.EmailFrom)
 | 
			
		||||
	// Print valid ranges
 | 
			
		||||
	if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO {
 | 
			
		||||
		fmt.Println("Valid subnet ranges:", util.GetSubnetRangesString())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	app.GET("/_health", handler.Health())
 | 
			
		||||
	app.GET("/logout", handler.Logout(), handler.ValidSession)
 | 
			
		||||
	app.POST("/new-client", handler.NewClient(db), handler.ValidSession)
 | 
			
		||||
	app.POST("/update-client", handler.UpdateClient(db), handler.ValidSession)
 | 
			
		||||
	app.POST("/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession)
 | 
			
		||||
	app.POST("/client/set-status", handler.SetClientStatus(db), handler.ValidSession)
 | 
			
		||||
	app.POST("/remove-client", handler.RemoveClient(db), handler.ValidSession)
 | 
			
		||||
	app.GET("/download", handler.DownloadClient(db), handler.ValidSession)
 | 
			
		||||
	app.GET("/wg-server", handler.WireGuardServer(db), handler.ValidSession)
 | 
			
		||||
	app.POST("wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession)
 | 
			
		||||
	app.POST("wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession)
 | 
			
		||||
	app.GET("/global-settings", handler.GlobalSettings(db), handler.ValidSession)
 | 
			
		||||
	app.POST("/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession)
 | 
			
		||||
	app.GET("/status", handler.Status(db), handler.ValidSession)
 | 
			
		||||
	app.GET("/api/clients", handler.GetClients(db), handler.ValidSession)
 | 
			
		||||
	app.GET("/api/client/:id", handler.GetClient(db), handler.ValidSession)
 | 
			
		||||
	app.GET("/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession)
 | 
			
		||||
	app.GET("/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession)
 | 
			
		||||
	app.GET("/api/apply-wg-config", handler.ApplyServerConfig(db, tmplBox), handler.ValidSession)
 | 
			
		||||
	// register routes
 | 
			
		||||
	app := router.New(tmplDir, extraData, util.SessionSecret)
 | 
			
		||||
 | 
			
		||||
	// servers other static files
 | 
			
		||||
	app.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler)))
 | 
			
		||||
	app.GET(util.BasePath, handler.WireGuardClients(db), handler.ValidSession, handler.RefreshSession)
 | 
			
		||||
 | 
			
		||||
	app.Logger.Fatal(app.Start(util.BindAddress))
 | 
			
		||||
	// Important: Make sure that all non-GET routes check the request content type using handler.ContentTypeJson to
 | 
			
		||||
	// mitigate CSRF attacks. This is effective, because browsers don't allow setting the Content-Type header on
 | 
			
		||||
	// cross-origin requests.
 | 
			
		||||
 | 
			
		||||
	if !util.DisableLogin {
 | 
			
		||||
		app.GET(util.BasePath+"/login", handler.LoginPage())
 | 
			
		||||
		app.POST(util.BasePath+"/login", handler.Login(db), handler.ContentTypeJson)
 | 
			
		||||
		app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession)
 | 
			
		||||
		app.GET(util.BasePath+"/profile", handler.LoadProfile(), handler.ValidSession, handler.RefreshSession)
 | 
			
		||||
		app.GET(util.BasePath+"/users-settings", handler.UsersSettings(), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin)
 | 
			
		||||
		app.POST(util.BasePath+"/update-user", handler.UpdateUser(db), handler.ValidSession, handler.ContentTypeJson)
 | 
			
		||||
		app.POST(util.BasePath+"/create-user", handler.CreateUser(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
 | 
			
		||||
		app.POST(util.BasePath+"/remove-user", handler.RemoveUser(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
 | 
			
		||||
		app.GET(util.BasePath+"/get-users", handler.GetUsers(db), handler.ValidSession, handler.NeedsAdmin)
 | 
			
		||||
		app.GET(util.BasePath+"/api/user/:username", handler.GetUser(db), handler.ValidSession)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var sendmail emailer.Emailer
 | 
			
		||||
	if util.SendgridApiKey != "" {
 | 
			
		||||
		sendmail = emailer.NewSendgridApiMail(util.SendgridApiKey, util.EmailFromName, util.EmailFrom)
 | 
			
		||||
	} else {
 | 
			
		||||
		sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpHelo, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	app.GET(util.BasePath+"/test-hash", handler.GetHashesChanges(db), handler.ValidSession)
 | 
			
		||||
	app.GET(util.BasePath+"/about", handler.AboutPage())
 | 
			
		||||
	app.GET(util.BasePath+"/_health", handler.Health())
 | 
			
		||||
	app.GET(util.BasePath+"/favicon", handler.Favicon())
 | 
			
		||||
	app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson)
 | 
			
		||||
	app.POST(util.BasePath+"/update-client", handler.UpdateClient(db), handler.ValidSession, handler.ContentTypeJson)
 | 
			
		||||
	app.POST(util.BasePath+"/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession, handler.ContentTypeJson)
 | 
			
		||||
	app.POST(util.BasePath+"/send-telegram-client", handler.SendTelegramClient(db), handler.ValidSession, handler.ContentTypeJson)
 | 
			
		||||
	app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson)
 | 
			
		||||
	app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson)
 | 
			
		||||
	app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession)
 | 
			
		||||
	app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin)
 | 
			
		||||
	app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
 | 
			
		||||
	app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
 | 
			
		||||
	app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin)
 | 
			
		||||
	app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
 | 
			
		||||
	app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession, handler.RefreshSession)
 | 
			
		||||
	app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession)
 | 
			
		||||
	app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession)
 | 
			
		||||
	app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession)
 | 
			
		||||
	app.GET(util.BasePath+"/api/subnet-ranges", handler.GetOrderedSubnetRanges(), handler.ValidSession)
 | 
			
		||||
	app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession)
 | 
			
		||||
	app.POST(util.BasePath+"/api/apply-wg-config", handler.ApplyServerConfig(db, tmplDir), handler.ValidSession, handler.ContentTypeJson)
 | 
			
		||||
	app.GET(util.BasePath+"/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession, handler.RefreshSession)
 | 
			
		||||
	app.POST(util.BasePath+"/wake_on_lan_host", handler.SaveWakeOnLanHost(db), handler.ValidSession, handler.ContentTypeJson)
 | 
			
		||||
	app.DELETE(util.BasePath+"/wake_on_lan_host/:mac_address", handler.DeleteWakeOnHost(db), handler.ValidSession, handler.ContentTypeJson)
 | 
			
		||||
	app.PUT(util.BasePath+"/wake_on_lan_host/:mac_address", handler.WakeOnHost(db), handler.ValidSession, handler.ContentTypeJson)
 | 
			
		||||
 | 
			
		||||
	// strip the "assets/" prefix from the embedded directory so files can be called directly without the "assets/"
 | 
			
		||||
	// prefix
 | 
			
		||||
	assetsDir, _ := fs.Sub(fs.FS(embeddedAssets), "assets")
 | 
			
		||||
	assetHandler := http.FileServer(http.FS(assetsDir))
 | 
			
		||||
	// serves other static files
 | 
			
		||||
	app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler)))
 | 
			
		||||
 | 
			
		||||
	initDeps := telegram.TgBotInitDependencies{
 | 
			
		||||
		DB:                             db,
 | 
			
		||||
		SendRequestedConfigsToTelegram: util.SendRequestedConfigsToTelegram,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	initTelegram(initDeps)
 | 
			
		||||
 | 
			
		||||
	if strings.HasPrefix(util.BindAddress, "unix://") {
 | 
			
		||||
		// Listen on unix domain socket.
 | 
			
		||||
		// https://github.com/labstack/echo/issues/830
 | 
			
		||||
		err := syscall.Unlink(util.BindAddress[6:])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			app.Logger.Fatalf("Cannot unlink unix socket: Error: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		l, err := net.Listen("unix", util.BindAddress[6:])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			app.Logger.Fatalf("Cannot create unix socket. Error: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		app.Listener = l
 | 
			
		||||
		app.Logger.Fatal(app.Start(""))
 | 
			
		||||
	} else {
 | 
			
		||||
		// Listen on TCP socket
 | 
			
		||||
		app.Logger.Fatal(app.Start(util.BindAddress))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initServerConfig(db store.IStore, tmplDir fs.FS) {
 | 
			
		||||
	settings, err := db.GetGlobalSettings()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Cannot get global settings: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := os.Stat(settings.ConfigFilePath); err == nil {
 | 
			
		||||
		// file exists, don't overwrite it implicitly
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	server, err := db.GetServer()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Cannot get server config: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	clients, err := db.GetClients(false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Cannot get client config: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	users, err := db.GetUsers()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Cannot get user config: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// write config file
 | 
			
		||||
	err = util.WriteWireGuardServerConfig(tmplDir, server, clients, users, settings)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Cannot create server config: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initTelegram(initDeps telegram.TgBotInitDependencies) {
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			err := telegram.Start(initDeps)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,18 +6,23 @@ import (
 | 
			
		|||
 | 
			
		||||
// Client model
 | 
			
		||||
type Client struct {
 | 
			
		||||
	ID           string    `json:"id"`
 | 
			
		||||
	PrivateKey   string    `json:"private_key"`
 | 
			
		||||
	PublicKey    string    `json:"public_key"`
 | 
			
		||||
	PresharedKey string    `json:"preshared_key"`
 | 
			
		||||
	Name         string    `json:"name"`
 | 
			
		||||
	Email        string    `json:"email"`
 | 
			
		||||
	AllocatedIPs []string  `json:"allocated_ips"`
 | 
			
		||||
	AllowedIPs   []string  `json:"allowed_ips"`
 | 
			
		||||
	UseServerDNS bool      `json:"use_server_dns"`
 | 
			
		||||
	Enabled      bool      `json:"enabled"`
 | 
			
		||||
	CreatedAt    time.Time `json:"created_at"`
 | 
			
		||||
	UpdatedAt    time.Time `json:"updated_at"`
 | 
			
		||||
	ID              string    `json:"id"`
 | 
			
		||||
	PrivateKey      string    `json:"private_key"`
 | 
			
		||||
	PublicKey       string    `json:"public_key"`
 | 
			
		||||
	PresharedKey    string    `json:"preshared_key"`
 | 
			
		||||
	Name            string    `json:"name"`
 | 
			
		||||
	TgUserid        string    `json:"telegram_userid"`
 | 
			
		||||
	Email           string    `json:"email"`
 | 
			
		||||
	SubnetRanges    []string  `json:"subnet_ranges,omitempty"`
 | 
			
		||||
	AllocatedIPs    []string  `json:"allocated_ips"`
 | 
			
		||||
	AllowedIPs      []string  `json:"allowed_ips"`
 | 
			
		||||
	ExtraAllowedIPs []string  `json:"extra_allowed_ips"`
 | 
			
		||||
	Endpoint        string    `json:"endpoint"`
 | 
			
		||||
	AdditionalNotes string    `json:"additional_notes"`
 | 
			
		||||
	UseServerDNS    bool      `json:"use_server_dns"`
 | 
			
		||||
	Enabled         bool      `json:"enabled"`
 | 
			
		||||
	CreatedAt       time.Time `json:"created_at"`
 | 
			
		||||
	UpdatedAt       time.Time `json:"updated_at"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClientData includes the Client and extra data
 | 
			
		||||
| 
						 | 
				
			
			@ -25,3 +30,9 @@ type ClientData struct {
 | 
			
		|||
	Client *Client
 | 
			
		||||
	QRCode string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type QRCodeSettings struct {
 | 
			
		||||
	Enabled    bool
 | 
			
		||||
	IncludeDNS bool
 | 
			
		||||
	IncludeMTU bool
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
package model
 | 
			
		||||
 | 
			
		||||
// ClientDefaults Defaults for creation of new clients used in the templates
 | 
			
		||||
type ClientDefaults struct {
 | 
			
		||||
	AllowedIps          []string
 | 
			
		||||
	ExtraAllowedIps     []string
 | 
			
		||||
	UseServerDNS        bool
 | 
			
		||||
	EnableAfterCreation bool
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -10,4 +10,11 @@ type Interface struct {
 | 
			
		|||
type BaseData struct {
 | 
			
		||||
	Active      string
 | 
			
		||||
	CurrentUser string
 | 
			
		||||
	Admin       bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClientServerHashes struct, to save hashes to detect changes
 | 
			
		||||
type ClientServerHashes struct {
 | 
			
		||||
	Client string `json:"client"`
 | 
			
		||||
	Server string `json:"server"`
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,5 +23,6 @@ type ServerInterface struct {
 | 
			
		|||
	ListenPort int       `json:"listen_port,string"` // ,string to get listen_port string input as int
 | 
			
		||||
	UpdatedAt  time.Time `json:"updated_at"`
 | 
			
		||||
	PostUp     string    `json:"post_up"`
 | 
			
		||||
	PreDown    string    `json:"pre_down"`
 | 
			
		||||
	PostDown   string    `json:"post_down"`
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,8 @@ type GlobalSetting struct {
 | 
			
		|||
	DNSServers          []string  `json:"dns_servers"`
 | 
			
		||||
	MTU                 int       `json:"mtu,string"`
 | 
			
		||||
	PersistentKeepalive int       `json:"persistent_keepalive,string"`
 | 
			
		||||
	FirewallMark        string    `json:"firewall_mark"`
 | 
			
		||||
	Table               string    `json:"table"`
 | 
			
		||||
	ConfigFilePath      string    `json:"config_file_path"`
 | 
			
		||||
	UpdatedAt           time.Time `json:"updated_at"`
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,4 +4,7 @@ package model
 | 
			
		|||
type User struct {
 | 
			
		||||
	Username string `json:"username"`
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
	// PasswordHash takes precedence over Password.
 | 
			
		||||
	PasswordHash string `json:"password_hash"`
 | 
			
		||||
	Admin        bool   `json:"admin"`
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type WakeOnLanHost struct {
 | 
			
		||||
	MacAddress string     `json:"MacAddress"`
 | 
			
		||||
	Name       string     `json:"Name"`
 | 
			
		||||
	LatestUsed *time.Time `json:"LatestUsed"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (host WakeOnLanHost) ResolveResourceName() (string, error) {
 | 
			
		||||
	resourceName := strings.Trim(host.MacAddress, " \t\r\n\000")
 | 
			
		||||
	if len(resourceName) == 0 {
 | 
			
		||||
		return "", errors.New("mac Address is Empty")
 | 
			
		||||
	}
 | 
			
		||||
	resourceName = strings.ToUpper(resourceName)
 | 
			
		||||
	resourceName = strings.ReplaceAll(resourceName, ":", "-")
 | 
			
		||||
 | 
			
		||||
	if _, err := net.ParseMAC(resourceName); err != nil {
 | 
			
		||||
		return "", errors.New("invalid mac address")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return resourceName, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const WakeOnLanHostCollectionName = "wake_on_lan_hosts"
 | 
			
		||||
| 
						 | 
				
			
			@ -4,7 +4,9 @@ set -e
 | 
			
		|||
DIR=$(dirname "$0")
 | 
			
		||||
 | 
			
		||||
# install node modules
 | 
			
		||||
yarn install --pure-lockfile --production
 | 
			
		||||
YARN=yarn
 | 
			
		||||
[ -x /usr/bin/lsb_release ] && [ -n "`lsb_release -i | grep Debian`" ] && YARN=yarnpkg
 | 
			
		||||
$YARN install --pure-lockfile --production
 | 
			
		||||
 | 
			
		||||
# Copy admin-lte dist
 | 
			
		||||
mkdir -p "${DIR}/assets/dist/js" "${DIR}/assets/dist/css" && \
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,21 +3,23 @@ package router
 | 
			
		|||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"text/template"
 | 
			
		||||
 | 
			
		||||
	rice "github.com/GeertJohan/go.rice"
 | 
			
		||||
	"github.com/gorilla/sessions"
 | 
			
		||||
	"github.com/labstack/echo-contrib/session"
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
	"github.com/labstack/echo/v4/middleware"
 | 
			
		||||
	"github.com/labstack/gommon/log"
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// TemplateRegistry is a custom html/template renderer for Echo framework
 | 
			
		||||
type TemplateRegistry struct {
 | 
			
		||||
	templates map[string]*template.Template
 | 
			
		||||
	extraData map[string]string
 | 
			
		||||
	extraData map[string]interface{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Render e.Renderer interface
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +35,8 @@ func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c
 | 
			
		|||
		for k, v := range t.extraData {
 | 
			
		||||
			data.(map[string]interface{})[k] = v
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data.(map[string]interface{})["client_defaults"] = util.ClientDefaultsFromEnv()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// login page does not need the base layout
 | 
			
		||||
| 
						 | 
				
			
			@ -44,58 +48,106 @@ func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// New function
 | 
			
		||||
func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Echo {
 | 
			
		||||
func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo.Echo {
 | 
			
		||||
	e := echo.New()
 | 
			
		||||
	e.Use(session.Middleware(sessions.NewCookieStore(secret)))
 | 
			
		||||
 | 
			
		||||
	cookiePath := util.GetCookiePath()
 | 
			
		||||
 | 
			
		||||
	cookieStore := sessions.NewCookieStore(secret[:32], secret[32:])
 | 
			
		||||
	cookieStore.Options.Path = cookiePath
 | 
			
		||||
	cookieStore.Options.HttpOnly = true
 | 
			
		||||
	cookieStore.MaxAge(86400 * 7)
 | 
			
		||||
 | 
			
		||||
	e.Use(session.Middleware(cookieStore))
 | 
			
		||||
 | 
			
		||||
	// read html template file to string
 | 
			
		||||
	tmplBaseString, err := tmplBox.String("base.html")
 | 
			
		||||
	tmplBaseString, err := util.StringFromEmbedFile(tmplDir, "base.html")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tmplLoginString, err := tmplBox.String("login.html")
 | 
			
		||||
	tmplLoginString, err := util.StringFromEmbedFile(tmplDir, "login.html")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tmplClientsString, err := tmplBox.String("clients.html")
 | 
			
		||||
	tmplProfileString, err := util.StringFromEmbedFile(tmplDir, "profile.html")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tmplServerString, err := tmplBox.String("server.html")
 | 
			
		||||
	tmplClientsString, err := util.StringFromEmbedFile(tmplDir, "clients.html")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tmplGlobalSettingsString, err := tmplBox.String("global_settings.html")
 | 
			
		||||
	tmplServerString, err := util.StringFromEmbedFile(tmplDir, "server.html")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tmplStatusString, err := tmplBox.String("status.html")
 | 
			
		||||
	tmplGlobalSettingsString, err := util.StringFromEmbedFile(tmplDir, "global_settings.html")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tmplUsersSettingsString, err := util.StringFromEmbedFile(tmplDir, "users_settings.html")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tmplStatusString, err := util.StringFromEmbedFile(tmplDir, "status.html")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tmplWakeOnLanHostsString, err := util.StringFromEmbedFile(tmplDir, "wake_on_lan_hosts.html")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	aboutPageString, err := util.StringFromEmbedFile(tmplDir, "about.html")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// create template list
 | 
			
		||||
	funcs := template.FuncMap{
 | 
			
		||||
		"StringsJoin": strings.Join,
 | 
			
		||||
	}
 | 
			
		||||
	templates := make(map[string]*template.Template)
 | 
			
		||||
	templates["login.html"] = template.Must(template.New("login").Parse(tmplLoginString))
 | 
			
		||||
	templates["clients.html"] = template.Must(template.New("clients").Parse(tmplBaseString + tmplClientsString))
 | 
			
		||||
	templates["server.html"] = template.Must(template.New("server").Parse(tmplBaseString + tmplServerString))
 | 
			
		||||
	templates["global_settings.html"] = template.Must(template.New("global_settings").Parse(tmplBaseString + tmplGlobalSettingsString))
 | 
			
		||||
	templates["status.html"] = template.Must(template.New("status").Parse(tmplBaseString + tmplStatusString))
 | 
			
		||||
	templates["login.html"] = template.Must(template.New("login").Funcs(funcs).Parse(tmplLoginString))
 | 
			
		||||
	templates["profile.html"] = template.Must(template.New("profile").Funcs(funcs).Parse(tmplBaseString + tmplProfileString))
 | 
			
		||||
	templates["clients.html"] = template.Must(template.New("clients").Funcs(funcs).Parse(tmplBaseString + tmplClientsString))
 | 
			
		||||
	templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString))
 | 
			
		||||
	templates["global_settings.html"] = template.Must(template.New("global_settings").Funcs(funcs).Parse(tmplBaseString + tmplGlobalSettingsString))
 | 
			
		||||
	templates["users_settings.html"] = template.Must(template.New("users_settings").Funcs(funcs).Parse(tmplBaseString + tmplUsersSettingsString))
 | 
			
		||||
	templates["status.html"] = template.Must(template.New("status").Funcs(funcs).Parse(tmplBaseString + tmplStatusString))
 | 
			
		||||
	templates["wake_on_lan_hosts.html"] = template.Must(template.New("wake_on_lan_hosts").Funcs(funcs).Parse(tmplBaseString + tmplWakeOnLanHostsString))
 | 
			
		||||
	templates["about.html"] = template.Must(template.New("about").Funcs(funcs).Parse(tmplBaseString + aboutPageString))
 | 
			
		||||
 | 
			
		||||
	e.Logger.SetLevel(log.DEBUG)
 | 
			
		||||
	lvl, err := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	logConfig := middleware.DefaultLoggerConfig
 | 
			
		||||
	logConfig.Skipper = func(c echo.Context) bool {
 | 
			
		||||
		resp := c.Response()
 | 
			
		||||
		if resp.Status >= 500 && lvl > log.ERROR { // do not log if response is 5XX but log level is higher than ERROR
 | 
			
		||||
			return true
 | 
			
		||||
		} else if resp.Status >= 400 && lvl > log.WARN { // do not log if response is 4XX but log level is higher than WARN
 | 
			
		||||
			return true
 | 
			
		||||
		} else if lvl > log.DEBUG { // do not log if log level is higher than DEBUG
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	e.Logger.SetLevel(lvl)
 | 
			
		||||
	e.Pre(middleware.RemoveTrailingSlash())
 | 
			
		||||
	e.Use(middleware.Logger())
 | 
			
		||||
	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
 | 
			
		||||
		AllowOrigins: []string{"*"},
 | 
			
		||||
		AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
 | 
			
		||||
		AllowMethods: []string{echo.GET, echo.HEAD, echo.PUT, echo.PATCH, echo.POST, echo.DELETE},
 | 
			
		||||
	}))
 | 
			
		||||
	e.Use(middleware.LoggerWithConfig(logConfig))
 | 
			
		||||
	e.HideBanner = true
 | 
			
		||||
	e.HidePort = lvl > log.INFO // hide the port output if the log level is higher than INFO
 | 
			
		||||
	e.Validator = NewValidator()
 | 
			
		||||
	e.Renderer = &TemplateRegistry{
 | 
			
		||||
		templates: templates,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ import (
 | 
			
		|||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/sdomino/scribble"
 | 
			
		||||
| 
						 | 
				
			
			@ -32,16 +33,18 @@ func New(dbPath string) (*JsonDB, error) {
 | 
			
		|||
		dbPath: dbPath,
 | 
			
		||||
	}
 | 
			
		||||
	return &ans, nil
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) Init() error {
 | 
			
		||||
	var clientPath string = path.Join(o.dbPath, "clients")
 | 
			
		||||
	var serverPath string = path.Join(o.dbPath, "server")
 | 
			
		||||
	var serverInterfacePath string = path.Join(serverPath, "interfaces.json")
 | 
			
		||||
	var serverKeyPairPath string = path.Join(serverPath, "keypair.json")
 | 
			
		||||
	var globalSettingPath string = path.Join(serverPath, "global_settings.json")
 | 
			
		||||
	var userPath string = path.Join(serverPath, "users.json")
 | 
			
		||||
	var clientPath = path.Join(o.dbPath, "clients")
 | 
			
		||||
	var serverPath = path.Join(o.dbPath, "server")
 | 
			
		||||
	var userPath = path.Join(o.dbPath, "users")
 | 
			
		||||
	var wakeOnLanHostsPath = path.Join(o.dbPath, "wake_on_lan_hosts")
 | 
			
		||||
	var serverInterfacePath = path.Join(serverPath, "interfaces.json")
 | 
			
		||||
	var serverKeyPairPath = path.Join(serverPath, "keypair.json")
 | 
			
		||||
	var globalSettingPath = path.Join(serverPath, "global_settings.json")
 | 
			
		||||
	var hashesPath = path.Join(serverPath, "hashes.json")
 | 
			
		||||
 | 
			
		||||
	// create directories if they do not exist
 | 
			
		||||
	if _, err := os.Stat(clientPath); os.IsNotExist(err) {
 | 
			
		||||
		os.MkdirAll(clientPath, os.ModePerm)
 | 
			
		||||
| 
						 | 
				
			
			@ -49,19 +52,30 @@ func (o *JsonDB) Init() error {
 | 
			
		|||
	if _, err := os.Stat(serverPath); os.IsNotExist(err) {
 | 
			
		||||
		os.MkdirAll(serverPath, os.ModePerm)
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := os.Stat(userPath); os.IsNotExist(err) {
 | 
			
		||||
		os.MkdirAll(userPath, os.ModePerm)
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) {
 | 
			
		||||
		os.MkdirAll(wakeOnLanHostsPath, os.ModePerm)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// server's interface
 | 
			
		||||
	if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) {
 | 
			
		||||
		serverInterface := new(model.ServerInterface)
 | 
			
		||||
		serverInterface.Addresses = []string{util.DefaultServerAddress}
 | 
			
		||||
		serverInterface.ListenPort = util.DefaultServerPort
 | 
			
		||||
		serverInterface.Addresses = util.LookupEnvOrStrings(util.ServerAddressesEnvVar, []string{util.DefaultServerAddress})
 | 
			
		||||
		serverInterface.ListenPort = util.LookupEnvOrInt(util.ServerListenPortEnvVar, util.DefaultServerPort)
 | 
			
		||||
		serverInterface.PostUp = util.LookupEnvOrString(util.ServerPostUpScriptEnvVar, "")
 | 
			
		||||
		serverInterface.PostDown = util.LookupEnvOrString(util.ServerPostDownScriptEnvVar, "")
 | 
			
		||||
		serverInterface.UpdatedAt = time.Now().UTC()
 | 
			
		||||
		o.conn.Write("server", "interfaces", serverInterface)
 | 
			
		||||
		err := util.ManagePerms(serverInterfacePath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// server's key pair
 | 
			
		||||
	if _, err := os.Stat(serverKeyPairPath); os.IsNotExist(err) {
 | 
			
		||||
 | 
			
		||||
		key, err := wgtypes.GeneratePrivateKey()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return scribble.ErrMissingCollection
 | 
			
		||||
| 
						 | 
				
			
			@ -71,41 +85,152 @@ func (o *JsonDB) Init() error {
 | 
			
		|||
		serverKeyPair.PublicKey = key.PublicKey().String()
 | 
			
		||||
		serverKeyPair.UpdatedAt = time.Now().UTC()
 | 
			
		||||
		o.conn.Write("server", "keypair", serverKeyPair)
 | 
			
		||||
		err = util.ManagePerms(serverKeyPairPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// global settings
 | 
			
		||||
	if _, err := os.Stat(globalSettingPath); os.IsNotExist(err) {
 | 
			
		||||
 | 
			
		||||
		publicInterface, err := util.GetPublicIP()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		endpointAddress := util.LookupEnvOrString(util.EndpointAddressEnvVar, "")
 | 
			
		||||
		if endpointAddress == "" {
 | 
			
		||||
			// automatically find an external IP address
 | 
			
		||||
			publicInterface, err := util.GetPublicIP()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			endpointAddress = publicInterface.IPAddress
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		globalSetting := new(model.GlobalSetting)
 | 
			
		||||
		globalSetting.EndpointAddress = publicInterface.IPAddress
 | 
			
		||||
		globalSetting.DNSServers = []string{util.DefaultDNS}
 | 
			
		||||
		globalSetting.MTU = util.DefaultMTU
 | 
			
		||||
		globalSetting.PersistentKeepalive = util.DefaultPersistentKeepalive
 | 
			
		||||
		globalSetting.ConfigFilePath = util.DefaultConfigFilePath
 | 
			
		||||
		globalSetting.EndpointAddress = endpointAddress
 | 
			
		||||
		globalSetting.DNSServers = util.LookupEnvOrStrings(util.DNSEnvVar, []string{util.DefaultDNS})
 | 
			
		||||
		globalSetting.MTU = util.LookupEnvOrInt(util.MTUEnvVar, util.DefaultMTU)
 | 
			
		||||
		globalSetting.PersistentKeepalive = util.LookupEnvOrInt(util.PersistentKeepaliveEnvVar, util.DefaultPersistentKeepalive)
 | 
			
		||||
		globalSetting.FirewallMark = util.LookupEnvOrString(util.FirewallMarkEnvVar, util.DefaultFirewallMark)
 | 
			
		||||
		globalSetting.Table = util.LookupEnvOrString(util.TableEnvVar, util.DefaultTable)
 | 
			
		||||
		globalSetting.ConfigFilePath = util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath)
 | 
			
		||||
		globalSetting.UpdatedAt = time.Now().UTC()
 | 
			
		||||
		o.conn.Write("server", "global_settings", globalSetting)
 | 
			
		||||
		err := util.ManagePerms(globalSettingPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// hashes
 | 
			
		||||
	if _, err := os.Stat(hashesPath); os.IsNotExist(err) {
 | 
			
		||||
		clientServerHashes := new(model.ClientServerHashes)
 | 
			
		||||
		clientServerHashes.Client = "none"
 | 
			
		||||
		clientServerHashes.Server = "none"
 | 
			
		||||
		o.conn.Write("server", "hashes", clientServerHashes)
 | 
			
		||||
		err := util.ManagePerms(hashesPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// user info
 | 
			
		||||
	if _, err := os.Stat(userPath); os.IsNotExist(err) {
 | 
			
		||||
	results, err := o.conn.ReadAll("users")
 | 
			
		||||
	if err != nil || len(results) < 1 {
 | 
			
		||||
		user := new(model.User)
 | 
			
		||||
		user.Username = util.GetCredVar(util.UsernameEnvVar, util.DefaultUsername)
 | 
			
		||||
		user.Password = util.GetCredVar(util.PasswordEnvVar, util.DefaultPassword)
 | 
			
		||||
		o.conn.Write("server", "users", user)
 | 
			
		||||
		user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername)
 | 
			
		||||
		user.Admin = util.DefaultIsAdmin
 | 
			
		||||
		user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "")
 | 
			
		||||
		if user.PasswordHash == "" {
 | 
			
		||||
			user.PasswordHash = util.LookupEnvOrFile(util.PasswordHashFileEnvVar, "")
 | 
			
		||||
			if user.PasswordHash == "" {
 | 
			
		||||
				plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword)
 | 
			
		||||
				if plaintext == util.DefaultPassword {
 | 
			
		||||
					plaintext = util.LookupEnvOrFile(util.PasswordFileEnvVar, util.DefaultPassword)
 | 
			
		||||
				}
 | 
			
		||||
				hash, err := util.HashPassword(plaintext)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				user.PasswordHash = hash
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		o.conn.Write("users", user.Username, user)
 | 
			
		||||
		results, _ = o.conn.ReadAll("users")
 | 
			
		||||
		err = util.ManagePerms(path.Join(path.Join(o.dbPath, "users"), user.Username+".json"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// init cache
 | 
			
		||||
	for _, i := range results {
 | 
			
		||||
		user := model.User{}
 | 
			
		||||
 | 
			
		||||
		if err := json.Unmarshal([]byte(i), &user); err == nil {
 | 
			
		||||
			util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	clients, err := o.GetClients(false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	for _, cl := range clients {
 | 
			
		||||
		client := cl.Client
 | 
			
		||||
		if client.Enabled && len(client.TgUserid) > 0 {
 | 
			
		||||
			if userid, err := strconv.ParseInt(client.TgUserid, 10, 64); err == nil {
 | 
			
		||||
				util.UpdateTgToClientID(userid, client.ID)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUser func to query user info from the database
 | 
			
		||||
func (o *JsonDB) GetUser() (model.User, error) {
 | 
			
		||||
// GetUsers func to get all users from the database
 | 
			
		||||
func (o *JsonDB) GetUsers() ([]model.User, error) {
 | 
			
		||||
	var users []model.User
 | 
			
		||||
	results, err := o.conn.ReadAll("users")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return users, err
 | 
			
		||||
	}
 | 
			
		||||
	for _, i := range results {
 | 
			
		||||
		user := model.User{}
 | 
			
		||||
 | 
			
		||||
		if err := json.Unmarshal(i, &user); err != nil {
 | 
			
		||||
			return users, fmt.Errorf("cannot decode user json structure: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		users = append(users, user)
 | 
			
		||||
	}
 | 
			
		||||
	return users, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserByName func to get single user from the database
 | 
			
		||||
func (o *JsonDB) GetUserByName(username string) (model.User, error) {
 | 
			
		||||
	user := model.User{}
 | 
			
		||||
	return user, o.conn.Read("server", "users", &user)
 | 
			
		||||
 | 
			
		||||
	if err := o.conn.Read("users", username, &user); err != nil {
 | 
			
		||||
		return user, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return user, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveUser func to save user in the database
 | 
			
		||||
func (o *JsonDB) SaveUser(user model.User) error {
 | 
			
		||||
	userPath := path.Join(path.Join(o.dbPath, "users"), user.Username+".json")
 | 
			
		||||
	output := o.conn.Write("users", user.Username, user)
 | 
			
		||||
	err := util.ManagePerms(userPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
 | 
			
		||||
	return output
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteUser func to remove user from the database
 | 
			
		||||
func (o *JsonDB) DeleteUser(username string) error {
 | 
			
		||||
	delete(util.DBUsersToCRC32, username)
 | 
			
		||||
	return o.conn.Delete("users", username)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetGlobalSettings func to query global settings from the database
 | 
			
		||||
| 
						 | 
				
			
			@ -114,7 +239,7 @@ func (o *JsonDB) GetGlobalSettings() (model.GlobalSetting, error) {
 | 
			
		|||
	return settings, o.conn.Read("server", "global_settings", &settings)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetServer func to query Server setting from the database
 | 
			
		||||
// GetServer func to query Server settings from the database
 | 
			
		||||
func (o *JsonDB) GetServer() (model.Server, error) {
 | 
			
		||||
	server := model.Server{}
 | 
			
		||||
	// read server interface information
 | 
			
		||||
| 
						 | 
				
			
			@ -138,7 +263,7 @@ func (o *JsonDB) GetServer() (model.Server, error) {
 | 
			
		|||
func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) {
 | 
			
		||||
	var clients []model.ClientData
 | 
			
		||||
 | 
			
		||||
	// read all client json file in "clients" directory
 | 
			
		||||
	// read all client json files in "clients" directory
 | 
			
		||||
	records, err := o.conn.ReadAll("clients")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return clients, err
 | 
			
		||||
| 
						 | 
				
			
			@ -150,18 +275,18 @@ func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) {
 | 
			
		|||
		clientData := model.ClientData{}
 | 
			
		||||
 | 
			
		||||
		// get client info
 | 
			
		||||
		if err := json.Unmarshal([]byte(f), &client); err != nil {
 | 
			
		||||
		if err := json.Unmarshal(f, &client); err != nil {
 | 
			
		||||
			return clients, fmt.Errorf("cannot decode client json structure: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// generate client qrcode image in base64
 | 
			
		||||
		if hasQRCode {
 | 
			
		||||
		if hasQRCode && client.PrivateKey != "" {
 | 
			
		||||
			server, _ := o.GetServer()
 | 
			
		||||
			globalSettings, _ := o.GetGlobalSettings()
 | 
			
		||||
 | 
			
		||||
			png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				clientData.QRCode = "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte(png))
 | 
			
		||||
				clientData.QRCode = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
 | 
			
		||||
			} else {
 | 
			
		||||
				fmt.Print("Cannot generate QR code: ", err)
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -175,7 +300,7 @@ func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) {
 | 
			
		|||
	return clients, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) GetClientByID(clientID string, hasQRCode bool) (model.ClientData, error) {
 | 
			
		||||
func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSettings) (model.ClientData, error) {
 | 
			
		||||
	client := model.Client{}
 | 
			
		||||
	clientData := model.ClientData{}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -185,13 +310,20 @@ func (o *JsonDB) GetClientByID(clientID string, hasQRCode bool) (model.ClientDat
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	// generate client qrcode image in base64
 | 
			
		||||
	if hasQRCode {
 | 
			
		||||
	if qrCodeSettings.Enabled && client.PrivateKey != "" {
 | 
			
		||||
		server, _ := o.GetServer()
 | 
			
		||||
		globalSettings, _ := o.GetGlobalSettings()
 | 
			
		||||
		client := client
 | 
			
		||||
		if !qrCodeSettings.IncludeDNS {
 | 
			
		||||
			globalSettings.DNSServers = []string{}
 | 
			
		||||
		}
 | 
			
		||||
		if !qrCodeSettings.IncludeMTU {
 | 
			
		||||
			globalSettings.MTU = 0
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			clientData.QRCode = "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte(png))
 | 
			
		||||
			clientData.QRCode = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
 | 
			
		||||
		} else {
 | 
			
		||||
			fmt.Print("Cannot generate QR code: ", err)
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -203,21 +335,76 @@ func (o *JsonDB) GetClientByID(clientID string, hasQRCode bool) (model.ClientDat
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) SaveClient(client model.Client) error {
 | 
			
		||||
	return o.conn.Write("clients", client.ID, client)
 | 
			
		||||
	clientPath := path.Join(path.Join(o.dbPath, "clients"), client.ID+".json")
 | 
			
		||||
	output := o.conn.Write("clients", client.ID, client)
 | 
			
		||||
	if output == nil {
 | 
			
		||||
		if client.Enabled && len(client.TgUserid) > 0 {
 | 
			
		||||
			if userid, err := strconv.ParseInt(client.TgUserid, 10, 64); err == nil {
 | 
			
		||||
				util.UpdateTgToClientID(userid, client.ID)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			util.RemoveTgToClientID(client.ID)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		util.RemoveTgToClientID(client.ID)
 | 
			
		||||
	}
 | 
			
		||||
	err := util.ManagePerms(clientPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return output
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) DeleteClient(clientID string) error {
 | 
			
		||||
	util.RemoveTgToClientID(clientID)
 | 
			
		||||
	return o.conn.Delete("clients", clientID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) SaveServerInterface(serverInterface model.ServerInterface) error {
 | 
			
		||||
	return o.conn.Write("server", "interfaces", serverInterface)
 | 
			
		||||
	serverInterfacePath := path.Join(path.Join(o.dbPath, "server"), "interfaces.json")
 | 
			
		||||
	output := o.conn.Write("server", "interfaces", serverInterface)
 | 
			
		||||
	err := util.ManagePerms(serverInterfacePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return output
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error {
 | 
			
		||||
	return o.conn.Write("server", "keypair", serverKeyPair)
 | 
			
		||||
	serverKeyPairPath := path.Join(path.Join(o.dbPath, "server"), "keypair.json")
 | 
			
		||||
	output := o.conn.Write("server", "keypair", serverKeyPair)
 | 
			
		||||
	err := util.ManagePerms(serverKeyPairPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return output
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error {
 | 
			
		||||
	return o.conn.Write("server", "global_settings", globalSettings)
 | 
			
		||||
	globalSettingsPath := path.Join(path.Join(o.dbPath, "server"), "global_settings.json")
 | 
			
		||||
	output := o.conn.Write("server", "global_settings", globalSettings)
 | 
			
		||||
	err := util.ManagePerms(globalSettingsPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return output
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) GetPath() string {
 | 
			
		||||
	return o.dbPath
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) GetHashes() (model.ClientServerHashes, error) {
 | 
			
		||||
	hashes := model.ClientServerHashes{}
 | 
			
		||||
	return hashes, o.conn.Read("server", "hashes", &hashes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) SaveHashes(hashes model.ClientServerHashes) error {
 | 
			
		||||
	hashesPath := path.Join(path.Join(o.dbPath, "server"), "hashes.json")
 | 
			
		||||
	output := o.conn.Write("server", "hashes", hashes)
 | 
			
		||||
	err := util.ManagePerms(hashesPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return output
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,88 @@
 | 
			
		|||
package jsondb
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/model"
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) GetWakeOnLanHosts() ([]model.WakeOnLanHost, error) {
 | 
			
		||||
	var hosts []model.WakeOnLanHost
 | 
			
		||||
 | 
			
		||||
	// read all client json file in "hosts" directory
 | 
			
		||||
	records, err := o.conn.ReadAll(model.WakeOnLanHostCollectionName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return hosts, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// build the ClientData list
 | 
			
		||||
	for _, f := range records {
 | 
			
		||||
		host := model.WakeOnLanHost{}
 | 
			
		||||
 | 
			
		||||
		// get client info
 | 
			
		||||
		if err := json.Unmarshal(f, &host); err != nil {
 | 
			
		||||
			return hosts, fmt.Errorf("cannot decode client json structure: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// create the list of hosts and their qrcode data
 | 
			
		||||
		hosts = append(hosts, host)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return hosts, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) GetWakeOnLanHost(macAddress string) (*model.WakeOnLanHost, error) {
 | 
			
		||||
	host := &model.WakeOnLanHost{
 | 
			
		||||
		MacAddress: macAddress,
 | 
			
		||||
	}
 | 
			
		||||
	resourceName, err := host.ResolveResourceName()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = o.conn.Read(model.WakeOnLanHostCollectionName, resourceName, host)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		host = nil
 | 
			
		||||
	}
 | 
			
		||||
	return host, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) DeleteWakeOnHostLanHost(macAddress string) error {
 | 
			
		||||
	host := &model.WakeOnLanHost{
 | 
			
		||||
		MacAddress: macAddress,
 | 
			
		||||
	}
 | 
			
		||||
	resourceName, err := host.ResolveResourceName()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return o.conn.Delete(model.WakeOnLanHostCollectionName, resourceName)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) SaveWakeOnLanHost(host model.WakeOnLanHost) error {
 | 
			
		||||
	resourceName, err := host.ResolveResourceName()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wakeOnLanHostPath := path.Join(path.Join(o.dbPath, model.WakeOnLanHostCollectionName), resourceName+".json")
 | 
			
		||||
	output := o.conn.Write(model.WakeOnLanHostCollectionName, resourceName, host)
 | 
			
		||||
	err = util.ManagePerms(wakeOnLanHostPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return output
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *JsonDB) DeleteWakeOnHost(host model.WakeOnLanHost) error {
 | 
			
		||||
	resourceName, err := host.ResolveResourceName()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return o.conn.Delete(model.WakeOnLanHostCollectionName, resourceName)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -6,14 +6,25 @@ import (
 | 
			
		|||
 | 
			
		||||
type IStore interface {
 | 
			
		||||
	Init() error
 | 
			
		||||
	GetUser() (model.User, error)
 | 
			
		||||
	GetUsers() ([]model.User, error)
 | 
			
		||||
	GetUserByName(username string) (model.User, error)
 | 
			
		||||
	SaveUser(user model.User) error
 | 
			
		||||
	DeleteUser(username string) error
 | 
			
		||||
	GetGlobalSettings() (model.GlobalSetting, error)
 | 
			
		||||
	GetServer() (model.Server, error)
 | 
			
		||||
	GetClients(hasQRCode bool) ([]model.ClientData, error)
 | 
			
		||||
	GetClientByID(clientID string, hasQRCode bool) (model.ClientData, error)
 | 
			
		||||
	GetClientByID(clientID string, qrCode model.QRCodeSettings) (model.ClientData, error)
 | 
			
		||||
	SaveClient(client model.Client) error
 | 
			
		||||
	DeleteClient(clientID string) error
 | 
			
		||||
	SaveServerInterface(serverInterface model.ServerInterface) error
 | 
			
		||||
	SaveServerKeyPair(serverKeyPair model.ServerKeypair) error
 | 
			
		||||
	SaveGlobalSettings(globalSettings model.GlobalSetting) error
 | 
			
		||||
	GetWakeOnLanHosts() ([]model.WakeOnLanHost, error)
 | 
			
		||||
	GetWakeOnLanHost(macAddress string) (*model.WakeOnLanHost, error)
 | 
			
		||||
	DeleteWakeOnHostLanHost(macAddress string) error
 | 
			
		||||
	SaveWakeOnLanHost(host model.WakeOnLanHost) error
 | 
			
		||||
	DeleteWakeOnHost(host model.WakeOnLanHost) error
 | 
			
		||||
	GetPath() string
 | 
			
		||||
	SaveHashes(hashes model.ClientServerHashes) error
 | 
			
		||||
	GetHashes() (model.ClientServerHashes, error)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,161 @@
 | 
			
		|||
package telegram
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/NicoNex/echotron/v3"
 | 
			
		||||
	"github.com/labstack/gommon/log"
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/store"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type SendRequestedConfigsToTelegram func(db store.IStore, userid int64) []string
 | 
			
		||||
 | 
			
		||||
type TgBotInitDependencies struct {
 | 
			
		||||
	DB                             store.IStore
 | 
			
		||||
	SendRequestedConfigsToTelegram SendRequestedConfigsToTelegram
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	Token            string
 | 
			
		||||
	AllowConfRequest bool
 | 
			
		||||
	FloodWait        int
 | 
			
		||||
	LogLevel         log.Lvl
 | 
			
		||||
 | 
			
		||||
	Bot      *echotron.API
 | 
			
		||||
	BotMutex sync.RWMutex
 | 
			
		||||
 | 
			
		||||
	floodWait        = make(map[int64]int64)
 | 
			
		||||
	floodMessageSent = make(map[int64]struct{})
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Start(initDeps TgBotInitDependencies) (err error) {
 | 
			
		||||
	ticker := time.NewTicker(time.Minute)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			BotMutex.Lock()
 | 
			
		||||
			Bot = nil
 | 
			
		||||
			BotMutex.Unlock()
 | 
			
		||||
			ticker.Stop()
 | 
			
		||||
		}
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
			err = fmt.Errorf("[PANIC] recovered from panic: %v", r)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	token := Token
 | 
			
		||||
	if token == "" || len(token) < 30 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bot := echotron.NewAPI(token)
 | 
			
		||||
 | 
			
		||||
	res, err := bot.GetMe()
 | 
			
		||||
	if !res.Ok || err != nil {
 | 
			
		||||
		log.Warnf("[Telegram] Unable to connect to bot.\n%v\n%v", res.Description, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	BotMutex.Lock()
 | 
			
		||||
	Bot = &bot
 | 
			
		||||
	BotMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	if LogLevel <= log.INFO {
 | 
			
		||||
		fmt.Printf("[Telegram] Authorized as %s\n", res.Result.Username)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		for range ticker.C {
 | 
			
		||||
			updateFloodWait()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	if !AllowConfRequest {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updatesChan := echotron.PollingUpdatesOptions(token, false, echotron.UpdateOptions{AllowedUpdates: []echotron.UpdateType{echotron.MessageUpdate}})
 | 
			
		||||
	for update := range updatesChan {
 | 
			
		||||
		if update.Message != nil {
 | 
			
		||||
			userid := update.Message.Chat.ID
 | 
			
		||||
			if _, wait := floodWait[userid]; wait {
 | 
			
		||||
				if _, notified := floodMessageSent[userid]; notified {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				floodMessageSent[userid] = struct{}{}
 | 
			
		||||
				_, err := bot.SendMessage(
 | 
			
		||||
					fmt.Sprintf("You can only request your configs once per %d minutes", FloodWait),
 | 
			
		||||
					userid,
 | 
			
		||||
					&echotron.MessageOptions{
 | 
			
		||||
						ReplyToMessageID: update.Message.ID,
 | 
			
		||||
					})
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Errorf("Failed to send telegram message. Error %v", err)
 | 
			
		||||
				}
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			floodWait[userid] = time.Now().Unix()
 | 
			
		||||
 | 
			
		||||
			failed := initDeps.SendRequestedConfigsToTelegram(initDeps.DB, userid)
 | 
			
		||||
			if len(failed) > 0 {
 | 
			
		||||
				messageText := "Failed to send configs:\n"
 | 
			
		||||
				for _, f := range failed {
 | 
			
		||||
					messageText += f + "\n"
 | 
			
		||||
				}
 | 
			
		||||
				_, err := bot.SendMessage(
 | 
			
		||||
					messageText,
 | 
			
		||||
					userid,
 | 
			
		||||
					&echotron.MessageOptions{
 | 
			
		||||
						ReplyToMessageID: update.Message.ID,
 | 
			
		||||
					})
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Errorf("Failed to send telegram message. Error %v", err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SendConfig(userid int64, clientName string, confData, qrData []byte, ignoreFloodWait bool) error {
 | 
			
		||||
	BotMutex.RLock()
 | 
			
		||||
	defer BotMutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
	if Bot == nil {
 | 
			
		||||
		return fmt.Errorf("telegram bot is not configured or not available")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, wait := floodWait[userid]; wait && !ignoreFloodWait {
 | 
			
		||||
		return fmt.Errorf("this client already got their config less than %d minutes ago", FloodWait)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !ignoreFloodWait {
 | 
			
		||||
		floodWait[userid] = time.Now().Unix()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	qrAttachment := echotron.NewInputFileBytes("qr.png", qrData)
 | 
			
		||||
	_, err := Bot.SendPhoto(qrAttachment, userid, &echotron.PhotoOptions{Caption: clientName})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error(err)
 | 
			
		||||
		return fmt.Errorf("unable to send qr picture")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	confAttachment := echotron.NewInputFileBytes(clientName+".conf", confData)
 | 
			
		||||
	_, err = Bot.SendDocument(confAttachment, userid, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error(err)
 | 
			
		||||
		return fmt.Errorf("unable to send conf file")
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func updateFloodWait() {
 | 
			
		||||
	thresholdTS := time.Now().Unix() - 60*int64(FloodWait)
 | 
			
		||||
	for userid, ts := range floodWait {
 | 
			
		||||
		if ts < thresholdTS {
 | 
			
		||||
			delete(floodWait, userid)
 | 
			
		||||
			delete(floodMessageSent, userid)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,145 @@
 | 
			
		|||
{{ define "title"}}
 | 
			
		||||
About
 | 
			
		||||
{{ end }}
 | 
			
		||||
 | 
			
		||||
{{ define "top_css"}}
 | 
			
		||||
{{ end }}
 | 
			
		||||
 | 
			
		||||
{{ define "username"}}
 | 
			
		||||
{{ .username }}
 | 
			
		||||
{{ end }}
 | 
			
		||||
 | 
			
		||||
{{ define "page_title"}}
 | 
			
		||||
About
 | 
			
		||||
{{ end }}
 | 
			
		||||
 | 
			
		||||
{{ define "page_content"}}
 | 
			
		||||
<section class="content">
 | 
			
		||||
    <div class="container-fluid">
 | 
			
		||||
        <!-- <h5 class="mt-4 mb-2">Global Settings</h5> -->
 | 
			
		||||
        <div class="row">
 | 
			
		||||
            <!-- left column -->
 | 
			
		||||
            <div class="col-md-6">
 | 
			
		||||
                <div class="card card-success">
 | 
			
		||||
                    <div class="card-header">
 | 
			
		||||
                        <h3 class="card-title">About Wireguard-UI</h3>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <!-- /.card-header -->
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="version" class="control-label">Current version</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="version" value="{{ .appVersion }}" readonly>
 | 
			
		||||
                        </div>
 | 
			
		||||
{{ if .gitCommit }}
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="version" class="control-label">git commit hash</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="version" value="{{ .gitCommit }}" readonly>
 | 
			
		||||
                        </div>
 | 
			
		||||
{{ end }}
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="currentReleaseDate" class="control-label">Current version release date</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="currentReleaseDate" readonly>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="latestRelease" class="control-label">Latest release</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="latestRelease" readonly>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="latestReleaseDate" class="control-label">Latest release date</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="latestReleaseDate" readonly>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="author" class="control-label">Author</label>
 | 
			
		||||
                            <div id="author">
 | 
			
		||||
                                <a id="authorLink">
 | 
			
		||||
                                    <img id="authorImage"
 | 
			
		||||
                                         style="width: 50px; height: 50px; border-radius: 50%; border: 1px solid #000;">
 | 
			
		||||
                                </a>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="contributors" class="control-label">Contributors</label>
 | 
			
		||||
                            <div id="contributors"></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <strong>Copyright ©
 | 
			
		||||
                            <script>document.write(new Date().getFullYear())</script>
 | 
			
		||||
                            <a href="https://github.com/ngoduykhanh/wireguard-ui">Wireguard UI</a>.
 | 
			
		||||
                        </strong> All rights reserved.
 | 
			
		||||
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!-- /.card -->
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- /.row -->
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
{{ end }}
 | 
			
		||||
 | 
			
		||||
{{ define "bottom_js"}}
 | 
			
		||||
<script>
 | 
			
		||||
    $(document).ready(function () {
 | 
			
		||||
 | 
			
		||||
        $.ajax({
 | 
			
		||||
            cache: false,
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
            url: 'https://api.github.com/repos/ngoduykhanh/wireguard-ui/releases/tags/' + $("#version").val(),
 | 
			
		||||
            dataType: 'json',
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            success: function (data) {
 | 
			
		||||
                $("#currentReleaseDate").attr("value", data.published_at.split("T")[0]);
 | 
			
		||||
 | 
			
		||||
            },
 | 
			
		||||
            error: function (jqXHR, exception) {
 | 
			
		||||
                $("#currentReleaseDate").attr("value", "Could not find this version on GitHub.com");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        $.ajax({
 | 
			
		||||
            cache: false,
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
            url: 'https://api.github.com/repos/ngoduykhanh/wireguard-ui/releases/latest',
 | 
			
		||||
            dataType: 'json',
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            success: function (data) {
 | 
			
		||||
                $("#latestRelease").attr("value", data.tag_name);
 | 
			
		||||
                $("#latestReleaseDate").attr("value", data.published_at.split("T")[0]);
 | 
			
		||||
                $("#author").attr("value", data.author.login);
 | 
			
		||||
                $("#authorImage").attr("src", data.author.avatar_url);
 | 
			
		||||
                $("#authorImage").after("<b>  " + data.author.login + "</b>");
 | 
			
		||||
                $("#authorLink").attr("href", data.author.html_url);
 | 
			
		||||
 | 
			
		||||
            },
 | 
			
		||||
            error: function (jqXHR, exception) {
 | 
			
		||||
                $("#latestRelease").attr("value", "Could not connect to GitHub.com");
 | 
			
		||||
                $("#latestReleaseDate").attr("value", "Could not connect to GitHub.com");
 | 
			
		||||
                $("#author").attr("value", "Could not connect to GitHub.com");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $.ajax({
 | 
			
		||||
            cache: false,
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
            url: 'https://api.github.com/repos/ngoduykhanh/wireguard-ui/contributors',
 | 
			
		||||
            dataType: 'json',
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            success: function (data) {
 | 
			
		||||
                data.forEach(contributor => $("#contributors").append("<a href=\"" + contributor.html_url + "\" title=\"" + contributor.login + "\">" +
 | 
			
		||||
                    "<img src=\"" + contributor.avatar_url + "\" style=\"width: 50px; height: 50px; border-radius: 50%; border: 1px solid #000; margin: 5px;\"/></a>"));
 | 
			
		||||
            },
 | 
			
		||||
            error: function (jqXHR, exception) {
 | 
			
		||||
                $("#contributors").html("<p>Could not connect to GitHub.com</p>");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(document).ajaxStop(function () {
 | 
			
		||||
        if (Date.parse($("#currentReleaseDate").val()) < Date.parse($("#latestReleaseDate").val())) {
 | 
			
		||||
            $("#currentReleaseDate").after("<p style=\"color:red\">Current version is out of date</p>")
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
{{ end }}
 | 
			
		||||
| 
						 | 
				
			
			@ -8,21 +8,23 @@
 | 
			
		|||
    <title>{{template "title" .}}</title>
 | 
			
		||||
    <!-- Tell the browser to be responsive to screen width -->
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
    <!-- Favicon -->
 | 
			
		||||
    <link rel="icon" href="{{.basePath}}/favicon">
 | 
			
		||||
 | 
			
		||||
    <!-- Font Awesome -->
 | 
			
		||||
    <link rel="stylesheet" href="static/plugins/fontawesome-free/css/all.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="{{.basePath}}/static/plugins/fontawesome-free/css/all.min.css">
 | 
			
		||||
    <!-- iCheck for checkboxes and radio inputs -->
 | 
			
		||||
    <link rel="stylesheet" href="static/plugins/icheck-bootstrap/icheck-bootstrap.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="{{.basePath}}/static/plugins/icheck-bootstrap/icheck-bootstrap.min.css">
 | 
			
		||||
    <!-- Select2 -->
 | 
			
		||||
    <link rel="stylesheet" href="static/plugins/select2/css/select2.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="{{.basePath}}/static/plugins/select2/css/select2.min.css">
 | 
			
		||||
    <!-- Toastr -->
 | 
			
		||||
    <link rel="stylesheet" href="static/plugins/toastr/toastr.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="{{.basePath}}/static/plugins/toastr/toastr.min.css">
 | 
			
		||||
    <!-- Jquery Tags Input -->
 | 
			
		||||
    <link rel="stylesheet" href="static/plugins/jquery-tags-input/dist/jquery.tagsinput.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="{{.basePath}}/static/plugins/jquery-tags-input/dist/jquery.tagsinput.min.css">
 | 
			
		||||
    <!-- Ionicons -->
 | 
			
		||||
    <link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
 | 
			
		||||
    <!-- overlayScrollbars -->
 | 
			
		||||
    <link rel="stylesheet" href="static/dist/css/adminlte.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="{{.basePath}}/static/dist/css/adminlte.min.css">
 | 
			
		||||
    <!-- Google Font: Source Sans Pro -->
 | 
			
		||||
    <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet">
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -44,28 +46,39 @@
 | 
			
		|||
            </ul>
 | 
			
		||||
 | 
			
		||||
            <!-- SEARCH FORM -->
 | 
			
		||||
<!--            <form class="form-inline ml-3">-->
 | 
			
		||||
<!--                <div class="input-group input-group-sm">-->
 | 
			
		||||
<!--                    <input class="form-control form-control-navbar" type="search" placeholder="Search"-->
 | 
			
		||||
<!--                        aria-label="Search">-->
 | 
			
		||||
<!--                    <div class="input-group-append">-->
 | 
			
		||||
<!--                        <button class="btn btn-navbar" type="submit">-->
 | 
			
		||||
<!--                            <i class="fas fa-search"></i>-->
 | 
			
		||||
<!--                        </button>-->
 | 
			
		||||
<!--                    </div>-->
 | 
			
		||||
<!--                </div>-->
 | 
			
		||||
<!--            </form>-->
 | 
			
		||||
            <form class="form-inline ml-3" style="display: none" id="search-form">
 | 
			
		||||
                <div class="input-group input-group-sm">
 | 
			
		||||
                    <input class="form-control form-control-navbar" placeholder="Search"
 | 
			
		||||
                        aria-label="Search" id="search-input">
 | 
			
		||||
                    <div class="input-group-append">
 | 
			
		||||
                        <button class="btn-navbar" type="submit" disabled>
 | 
			
		||||
                            <i class="fas fa-search"></i>
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="form-group form-group-sm">
 | 
			
		||||
                    <select name="status-selector" id="status-selector" class="custom-select form-control-navbar" style="margin-left: 0.5em; height: 90%; font-size: 14px;">
 | 
			
		||||
                        <!-- THIS SECTION IS OVERRIDDEN BY JS. SEE updateSearchList() function in clients.html BEFORE EDITING -->
 | 
			
		||||
                        <option value="All">All</option>
 | 
			
		||||
                        <option value="Enabled">Enabled</option>
 | 
			
		||||
                        <option value="Disabled">Disabled</option>
 | 
			
		||||
                        <option value="Connected">Connected</option>
 | 
			
		||||
                        <option value="Disconnected">Disconnected</option>
 | 
			
		||||
                        <!-- THIS SECTION IS OVERRIDDEN BY JS. SEE updateSearchList() function in clients.html BEFORE EDITING -->
 | 
			
		||||
                    </select>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
 | 
			
		||||
            <!-- Right navbar links -->
 | 
			
		||||
            <div class="navbar-nav ml-auto">
 | 
			
		||||
                <button style="margin-left: 0.5em;" type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal"
 | 
			
		||||
                    data-target="#modal_new_client"><i class="nav-icon fas fa-plus"></i> New
 | 
			
		||||
                    Client</button>
 | 
			
		||||
                <button style="margin-left: 0.5em;" type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal"
 | 
			
		||||
                <button id="apply-config-button" style="margin-left: 0.5em; display: none;" type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal"
 | 
			
		||||
                    data-target="#modal_apply_config"><i class="nav-icon fas fa-check"></i> Apply
 | 
			
		||||
                    Config</button>
 | 
			
		||||
                {{if .baseData.CurrentUser}}
 | 
			
		||||
                <button onclick="location.href='/logout';" style="margin-left: 0.5em;" type="button"
 | 
			
		||||
                <button onclick="location.href='{{.basePath}}/logout';" style="margin-left: 0.5em;" type="button"
 | 
			
		||||
                    class="btn btn-outline-danger btn-sm"><i class="nav-icon fas fa-sign-out-alt"></i> Logout</button>
 | 
			
		||||
                {{end}}
 | 
			
		||||
            </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -75,10 +88,8 @@
 | 
			
		|||
        <!-- Main Sidebar Container -->
 | 
			
		||||
        <aside class="main-sidebar sidebar-dark-primary elevation-4">
 | 
			
		||||
            <!-- Brand Logo -->
 | 
			
		||||
            <a href="/" class="brand-link">
 | 
			
		||||
                <!-- <img src="static/dist/img/logo.png" alt="Wireguard UI"
 | 
			
		||||
                    class="brand-image img-circle elevation-3" style="opacity: .8"> -->
 | 
			
		||||
                <span class="brand-text font-weight-light">WIREGUARD UI</span>
 | 
			
		||||
            <a href="{{.basePath}}" class="brand-link">
 | 
			
		||||
                <span class="brand-text">  WIREGUARD UI</span>
 | 
			
		||||
            </a>
 | 
			
		||||
 | 
			
		||||
            <!-- Sidebar -->
 | 
			
		||||
| 
						 | 
				
			
			@ -89,45 +100,91 @@
 | 
			
		|||
                        <i class="nav-icon fas fa-2x fa-user"></i>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="info">
 | 
			
		||||
                        <a href="#" class="d-block">{{if .baseData.CurrentUser}} {{.baseData.CurrentUser}} {{else}} Administrator {{end}}</a>
 | 
			
		||||
                        {{if .baseData.CurrentUser}}
 | 
			
		||||
 | 
			
		||||
                        {{if .baseData.Admin}}
 | 
			
		||||
                        <a href="{{.basePath}}/profile" class="d-block">Administrator: {{.baseData.CurrentUser}}</a>
 | 
			
		||||
                        {{else}}
 | 
			
		||||
                        <a href="{{.basePath}}/profile" class="d-block">Manager: {{.baseData.CurrentUser}}</a>
 | 
			
		||||
                        {{end}}
 | 
			
		||||
 | 
			
		||||
                        {{else}}
 | 
			
		||||
                        <a href="#" class="d-block">Administrator</a>
 | 
			
		||||
                        {{end}}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- Sidebar Menu -->
 | 
			
		||||
                <nav class="mt-2">
 | 
			
		||||
                    <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
 | 
			
		||||
                        <li class="nav-header">MAIN</li>
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a href="/" class="nav-link {{if eq .baseData.Active ""}}active{{end}}">
 | 
			
		||||
                            <a href="{{.basePath}}/" class="nav-link {{if eq .baseData.Active ""}}active{{end}}">
 | 
			
		||||
                                <i class="nav-icon fas fa-user-secret"></i>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    Wireguard Clients
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </li>
 | 
			
		||||
 | 
			
		||||
                        {{if .baseData.Admin}}
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a href="/wg-server" class="nav-link {{if eq .baseData.Active "wg-server" }}active{{end}}">
 | 
			
		||||
                            <a href="{{.basePath}}/wg-server" class="nav-link {{if eq .baseData.Active "wg-server" }}active{{end}}">
 | 
			
		||||
                                <i class="nav-icon fas fa-server"></i>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    Wireguard Server
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </li>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                        <li class="nav-header">SETTINGS</li>
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a href="/global-settings" class="nav-link {{if eq .baseData.Active "global-settings" }}active{{end}}">
 | 
			
		||||
                            <a href="{{.basePath}}/global-settings" class="nav-link {{if eq .baseData.Active "global-settings" }}active{{end}}">
 | 
			
		||||
                                <i class="nav-icon fas fa-cog"></i>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    Global Settings
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        {{if not .loginDisabled}}
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a href="/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}">
 | 
			
		||||
                                <i class="nav-icon fas fa-user"></i>
 | 
			
		||||
                            <a href="{{.basePath}}/users-settings" class="nav-link {{if eq .baseData.Active "users-settings" }}active{{end}}">
 | 
			
		||||
                            <i class="nav-icon fas fa-cog"></i>
 | 
			
		||||
                            <p>
 | 
			
		||||
                                Users Settings
 | 
			
		||||
                            </p>
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        {{end}}
 | 
			
		||||
                        {{end}}
 | 
			
		||||
 | 
			
		||||
                        <li class="nav-header">UTILITIES</li>
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a href="{{.basePath}}/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}">
 | 
			
		||||
                                <i class="nav-icon fas fa-signal"></i>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    Status
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a href="{{.basePath}}/wake_on_lan_hosts" class="nav-link {{if eq .baseData.Active "wake_on_lan_hosts" }}active{{end}}">
 | 
			
		||||
                                <i class="nav-icon fas  fa-solid fa-power-off"></i>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    WoL Hosts
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li class="nav-header">ABOUT</li>
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a href="{{.basePath}}/about" class="nav-link {{if eq .baseData.Active "about" }}active{{end}}">
 | 
			
		||||
                            <i class="nav-icon fas  fa-solid fa-id-card"></i>
 | 
			
		||||
                            <p>
 | 
			
		||||
                                About
 | 
			
		||||
                            </p>
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </nav>
 | 
			
		||||
                <!-- /.sidebar-menu -->
 | 
			
		||||
| 
						 | 
				
			
			@ -154,18 +211,42 @@
 | 
			
		|||
                                <label for="client_email" class="control-label">Email</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="client_email" name="client_email">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label for="subnet_ranges" class="control-label">Subnet range</label>
 | 
			
		||||
                                <select id="subnet_ranges" class="select2"
 | 
			
		||||
                                    data-placeholder="Select a subnet range" style="width: 100%;">
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label for="client_allocated_ips" class="control-label">IP Allocation</label>
 | 
			
		||||
                                <input type="text" data-role="tagsinput" class="form-control" id="client_allocated_ips">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label for="client_allowed_ips" class="control-label">Allowed IPs</label>
 | 
			
		||||
                                <label for="client_allowed_ips" class="control-label">Allowed IPs
 | 
			
		||||
                                    <i class="fas fa-info-circle" data-toggle="tooltip"
 | 
			
		||||
                                       data-original-title="Specify a list of addresses that will get routed to the
 | 
			
		||||
                                       server. These addresses will be included in 'AllowedIPs' of client config">
 | 
			
		||||
                                    </i>
 | 
			
		||||
                                </label>
 | 
			
		||||
                                <input type="text" data-role="tagsinput" class="form-control" id="client_allowed_ips"
 | 
			
		||||
                                    value="0.0.0.0/0">
 | 
			
		||||
                                    value="{{ StringsJoin .client_defaults.AllowedIps "," }}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label for="client_extra_allowed_ips" class="control-label">Extra Allowed IPs
 | 
			
		||||
                                    <i class="fas fa-info-circle" data-toggle="tooltip"
 | 
			
		||||
                                       data-original-title="Specify a list of addresses that will get routed to the
 | 
			
		||||
                                       client. These addresses will be included in 'AllowedIPs' of WG server config">
 | 
			
		||||
                                    </i>
 | 
			
		||||
                                </label>
 | 
			
		||||
                                <input type="text" data-role="tagsinput" class="form-control" id="client_extra_allowed_ips" value="{{ StringsJoin .client_defaults.ExtraAllowedIps "," }}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label for="client_endpoint" class="control-label">Endpoint</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="client_endpoint" name="client_endpoint">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <div class="icheck-primary d-inline">
 | 
			
		||||
                                    <input type="checkbox" id="use_server_dns" checked>
 | 
			
		||||
                                    <input type="checkbox" id="use_server_dns" {{ if .client_defaults.UseServerDNS }}checked{{ end }}>
 | 
			
		||||
                                    <label for="use_server_dns">
 | 
			
		||||
                                        Use server DNS
 | 
			
		||||
                                    </label>
 | 
			
		||||
| 
						 | 
				
			
			@ -173,12 +254,45 @@
 | 
			
		|||
                            </div>
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <div class="icheck-primary d-inline">
 | 
			
		||||
                                    <input type="checkbox" id="enabled" checked>
 | 
			
		||||
                                    <input type="checkbox" id="enabled" {{ if .client_defaults.EnableAfterCreation }}checked{{ end }}>
 | 
			
		||||
                                    <label for="enabled">
 | 
			
		||||
                                        Enable after creation
 | 
			
		||||
                                    </label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <details>
 | 
			
		||||
                                <summary><strong>Public and Preshared Keys</strong>
 | 
			
		||||
                                    <i class="fas fa-info-circle" data-toggle="tooltip"
 | 
			
		||||
                                       data-original-title="If you don't want to let the server generate and store the
 | 
			
		||||
                                       client's private key, you can manually specify its public and preshared key here
 | 
			
		||||
                                       . Note: QR code will not be generated">
 | 
			
		||||
                                    </i>
 | 
			
		||||
                                </summary>
 | 
			
		||||
                                <div class="form-group" style="margin-top: 1rem">
 | 
			
		||||
                                    <label for="client_public_key" class="control-label">
 | 
			
		||||
                                        Public Key
 | 
			
		||||
                                    </label>
 | 
			
		||||
                                    <input type="text" class="form-control" id="client_public_key" name="client_public_key" placeholder="Autogenerated" aria-invalid="false">
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-group">
 | 
			
		||||
                                    <label for="client_preshared_key" class="control-label">
 | 
			
		||||
                                        Preshared Key
 | 
			
		||||
                                    </label>
 | 
			
		||||
                                    <input type="text" class="form-control" id="client_preshared_key" name="client_preshared_key" placeholder="Autogenerated - enter "-" to skip generation">
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </details>
 | 
			
		||||
                            <details style="margin-top: 0.5rem;">
 | 
			
		||||
                                <summary><strong>Additional configuration</strong>
 | 
			
		||||
                                </summary>
 | 
			
		||||
                                <div class="form-group" style="margin-top: 0.5rem;">
 | 
			
		||||
                                    <label for="client_telegram_userid" class="control-label">Telegram userid</label>
 | 
			
		||||
                                    <input type="text" class="form-control" id="client_telegram_userid" name="client_telegram_userid">
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-group">
 | 
			
		||||
                                    <label for="additional_notes" class="control-label">Notes</label>
 | 
			
		||||
                                    <textarea class="form-control" style="min-height: 6rem;" id="additional_notes" name="additional_notes" placeholder="Additional notes about this client"></textarea>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </details>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-footer justify-content-between">
 | 
			
		||||
                            <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
 | 
			
		||||
| 
						 | 
				
			
			@ -233,15 +347,15 @@
 | 
			
		|||
            <!-- /.content -->
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- /.content-wrapper -->
 | 
			
		||||
 | 
			
		||||
        <!--
 | 
			
		||||
        <footer class="main-footer">
 | 
			
		||||
            <div class="float-right d-none d-sm-block">
 | 
			
		||||
                <b>Version</b> {{ .appVersion }}
 | 
			
		||||
            </div>
 | 
			
		||||
            <strong>Copyright © 2020 <a href="https://github.com/ngoduykhanh/wireguard-ui">Wireguard UI</a>.</strong> All rights
 | 
			
		||||
            <strong>Copyright © <script>document.write(new Date().getFullYear())</script> <a href="https://github.com/ngoduykhanh/wireguard-ui">Wireguard UI</a>.</strong> All rights
 | 
			
		||||
            reserved.
 | 
			
		||||
        </footer>
 | 
			
		||||
 | 
			
		||||
        -->
 | 
			
		||||
        <!-- Control Sidebar -->
 | 
			
		||||
        <aside class="control-sidebar control-sidebar-dark">
 | 
			
		||||
            <!-- Control sidebar content goes here -->
 | 
			
		||||
| 
						 | 
				
			
			@ -251,29 +365,86 @@
 | 
			
		|||
    <!-- ./wrapper -->
 | 
			
		||||
 | 
			
		||||
    <!-- jQuery -->
 | 
			
		||||
    <script src="static/plugins/jquery/jquery.min.js"></script>
 | 
			
		||||
    <script src="{{.basePath}}/static/plugins/jquery/jquery.min.js"></script>
 | 
			
		||||
    <!-- Bootstrap 4 -->
 | 
			
		||||
    <script src="static/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
 | 
			
		||||
    <script src="{{.basePath}}/static/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
 | 
			
		||||
    <!-- Select2 -->
 | 
			
		||||
    <script src="static/plugins/select2/js/select2.full.min.js"></script>
 | 
			
		||||
    <script src="{{.basePath}}/static/plugins/select2/js/select2.full.min.js"></script>
 | 
			
		||||
    <!-- jquery-validation -->
 | 
			
		||||
    <script src="static/plugins/jquery-validation/jquery.validate.min.js"></script>
 | 
			
		||||
    <script src="{{.basePath}}/static/plugins/jquery-validation/jquery.validate.min.js"></script>
 | 
			
		||||
    <!-- Toastr -->
 | 
			
		||||
    <script src="static/plugins/toastr/toastr.min.js"></script>
 | 
			
		||||
    <script src="{{.basePath}}/static/plugins/toastr/toastr.min.js"></script>
 | 
			
		||||
    <!-- Jquery Tags Input -->
 | 
			
		||||
    <script src="static/plugins/jquery-tags-input/dist/jquery.tagsinput.min.js"></script>
 | 
			
		||||
    <script src="{{.basePath}}/static/plugins/jquery-tags-input/dist/jquery.tagsinput.min.js"></script>
 | 
			
		||||
    <!-- AdminLTE App -->
 | 
			
		||||
    <script src="static/dist/js/adminlte.min.js"></script>
 | 
			
		||||
    <script src="{{.basePath}}/static/dist/js/adminlte.min.js"></script>
 | 
			
		||||
    <!-- Custom js -->
 | 
			
		||||
    <script src="static/custom/js/helper.js"></script>
 | 
			
		||||
    <script src="{{.basePath}}/static/custom/js/helper.js"></script>
 | 
			
		||||
    <script>
 | 
			
		||||
        // initialize all tooltips
 | 
			
		||||
        $(function () {
 | 
			
		||||
            $('[data-toggle="tooltip"]').tooltip()
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        $(document).ready(function () {
 | 
			
		||||
 | 
			
		||||
            addGlobalStyle(`
 | 
			
		||||
.toast-top-right-fix {
 | 
			
		||||
    top: 67px;
 | 
			
		||||
    right: 12px;
 | 
			
		||||
}
 | 
			
		||||
            `, 'toastrToastStyleFix')
 | 
			
		||||
 | 
			
		||||
            toastr.options.closeDuration = 100;
 | 
			
		||||
            // toastr.options.timeOut = 10000;
 | 
			
		||||
            toastr.options.positionClass = 'toast-top-right-fix';
 | 
			
		||||
 | 
			
		||||
            updateApplyConfigVisibility()
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        function addGlobalStyle(css, id) {
 | 
			
		||||
            if (!document.querySelector('#' + id)) {
 | 
			
		||||
                let head = document.head
 | 
			
		||||
                if (!head) { return }
 | 
			
		||||
                let style = document.createElement('style')
 | 
			
		||||
                style.type = 'text/css'
 | 
			
		||||
                style.id = id
 | 
			
		||||
                style.innerHTML = css
 | 
			
		||||
                head.appendChild(style)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function updateApplyConfigVisibility() {
 | 
			
		||||
                $.ajax({
 | 
			
		||||
                    cache: false,
 | 
			
		||||
                    method: 'GET',
 | 
			
		||||
                    url: '{{.basePath}}/test-hash',
 | 
			
		||||
                    dataType: 'json',
 | 
			
		||||
                    contentType: "application/json",
 | 
			
		||||
                    success: function(data) {
 | 
			
		||||
                        if (data.status) {
 | 
			
		||||
                            $("#apply-config-button").show()
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            $("#apply-config-button").hide()
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    error: function(jqXHR, exception) {
 | 
			
		||||
                        const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                        toastr.error(responseJson['message']);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // populateClient function for render new client info
 | 
			
		||||
        // on the client page.
 | 
			
		||||
        function populateClient(client_id) {
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'GET',
 | 
			
		||||
                url: '/api/client/' + client_id,
 | 
			
		||||
                url: '{{.basePath}}/api/client/' + client_id,
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                success: function (resp) {
 | 
			
		||||
| 
						 | 
				
			
			@ -290,9 +461,17 @@
 | 
			
		|||
        function submitNewClient() {
 | 
			
		||||
            const name = $("#client_name").val();
 | 
			
		||||
            const email = $("#client_email").val();
 | 
			
		||||
            const telegram_userid = $("#client_telegram_userid").val();
 | 
			
		||||
            const allocated_ips = $("#client_allocated_ips").val().split(",");
 | 
			
		||||
            const allowed_ips = $("#client_allowed_ips").val().split(",");
 | 
			
		||||
            const endpoint = $("#client_endpoint").val();
 | 
			
		||||
            let use_server_dns = false;
 | 
			
		||||
            let extra_allowed_ips = [];
 | 
			
		||||
 | 
			
		||||
            if ($("#client_extra_allowed_ips").val() !== "") {
 | 
			
		||||
                extra_allowed_ips = $("#client_extra_allowed_ips").val().split(",");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            if ($("#use_server_dns").is(':checked')){
 | 
			
		||||
                use_server_dns = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -303,14 +482,19 @@
 | 
			
		|||
            if ($("#enabled").is(':checked')){
 | 
			
		||||
                enabled = true;
 | 
			
		||||
            }
 | 
			
		||||
            const public_key = $("#client_public_key").val();
 | 
			
		||||
            const preshared_key = $("#client_preshared_key").val();
 | 
			
		||||
            
 | 
			
		||||
            const additional_notes = $("#additional_notes").val();
 | 
			
		||||
 | 
			
		||||
            const data = {"name": name, "email": email, "allocated_ips": allocated_ips, "allowed_ips": allowed_ips,
 | 
			
		||||
                "use_server_dns": use_server_dns, "enabled": enabled};
 | 
			
		||||
            const data = {"name": name, "email": email, "telegram_userid": telegram_userid, "allocated_ips": allocated_ips, "allowed_ips": allowed_ips,
 | 
			
		||||
                "extra_allowed_ips": extra_allowed_ips, "endpoint": endpoint, "use_server_dns": use_server_dns, "enabled": enabled,
 | 
			
		||||
                "public_key": public_key, "preshared_key": preshared_key, "additional_notes": additional_notes};
 | 
			
		||||
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                url: '/new-client',
 | 
			
		||||
                url: '{{.basePath}}/new-client',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
| 
						 | 
				
			
			@ -318,9 +502,10 @@
 | 
			
		|||
                    $("#modal_new_client").modal('hide');
 | 
			
		||||
                    toastr.success('Created new client successfully');
 | 
			
		||||
                    // Update the home page (clients page) after adding successfully
 | 
			
		||||
                    if (window.location.pathname === "/") {
 | 
			
		||||
                    if (window.location.pathname === "{{.basePath}}/") {
 | 
			
		||||
                        populateClient(resp.id);
 | 
			
		||||
                    }
 | 
			
		||||
                    updateApplyConfigVisibility()
 | 
			
		||||
                },
 | 
			
		||||
                error: function(jqXHR, exception) {
 | 
			
		||||
                    const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
| 
						 | 
				
			
			@ -331,19 +516,32 @@
 | 
			
		|||
 | 
			
		||||
        // updateIPAllocationSuggestion function for automatically fill
 | 
			
		||||
        // the IP Allocation input with suggested ip addresses
 | 
			
		||||
        function updateIPAllocationSuggestion() {
 | 
			
		||||
        function updateIPAllocationSuggestion(forceDefault = false) {
 | 
			
		||||
            let subnetRange = $("#subnet_ranges").select2('val');
 | 
			
		||||
 | 
			
		||||
            if (forceDefault || !subnetRange || subnetRange.length === 0) {
 | 
			
		||||
                subnetRange = '__default_any__'
 | 
			
		||||
            }
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'GET',
 | 
			
		||||
                url: '/api/suggest-client-ips',
 | 
			
		||||
                url: `{{.basePath}}/api/suggest-client-ips?sr=${subnetRange}`,
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                success: function(data) {
 | 
			
		||||
                    const allocated_ips = $("#client_allocated_ips").val().split(",");
 | 
			
		||||
                    allocated_ips.forEach(function (item, index) {
 | 
			
		||||
                        $('#client_allocated_ips').removeTag(escape(item));
 | 
			
		||||
                    })
 | 
			
		||||
                    data.forEach(function (item, index) {
 | 
			
		||||
                        $('#client_allocated_ips').addTag(item);
 | 
			
		||||
                    })
 | 
			
		||||
                },
 | 
			
		||||
                error: function(jqXHR, exception) {
 | 
			
		||||
                    const allocated_ips = $("#client_allocated_ips").val().split(",");
 | 
			
		||||
                    allocated_ips.forEach(function (item, index) {
 | 
			
		||||
                        $('#client_allocated_ips').removeTag(escape(item));
 | 
			
		||||
                    })
 | 
			
		||||
                    const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                    toastr.error(responseJson['message']);
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -362,6 +560,7 @@
 | 
			
		|||
            'defaultText': 'Add More',
 | 
			
		||||
            'removeWithBackspace': true,
 | 
			
		||||
            'minChars': 0,
 | 
			
		||||
            'minInputWidth': '100%',
 | 
			
		||||
            'placeholderColor': '#666666'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -373,6 +572,18 @@
 | 
			
		|||
            'defaultText': 'Add More',
 | 
			
		||||
            'removeWithBackspace': true,
 | 
			
		||||
            'minChars': 0,
 | 
			
		||||
            'minInputWidth': '100%',
 | 
			
		||||
            'placeholderColor': '#666666'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $("#client_extra_allowed_ips").tagsInput({
 | 
			
		||||
            'width': '100%',
 | 
			
		||||
            'height': '75%',
 | 
			
		||||
            'interactive': true,
 | 
			
		||||
            'defaultText': 'Add More',
 | 
			
		||||
            'removeWithBackspace': true,
 | 
			
		||||
            'minChars': 0,
 | 
			
		||||
            'minInputWidth': '100%',
 | 
			
		||||
            'placeholderColor': '#666666'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -413,21 +624,35 @@
 | 
			
		|||
            $("#modal_new_client").on('shown.bs.modal', function (e) {
 | 
			
		||||
                $("#client_name").val("");
 | 
			
		||||
                $("#client_email").val("");
 | 
			
		||||
                $("#client_public_key").val("");
 | 
			
		||||
                $("#client_preshared_key").val("");
 | 
			
		||||
                $("#client_allocated_ips").importTags('');
 | 
			
		||||
                updateIPAllocationSuggestion();
 | 
			
		||||
                $("#client_extra_allowed_ips").importTags('');
 | 
			
		||||
                $("#client_endpoint").val('');
 | 
			
		||||
                $("#client_telegram_userid").val('');
 | 
			
		||||
                $("#additional_notes").val('');
 | 
			
		||||
                updateSubnetRangesList("#subnet_ranges");
 | 
			
		||||
                updateIPAllocationSuggestion(true);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // handle subnet range select
 | 
			
		||||
        $('#subnet_ranges').on('select2:select', function (e) {
 | 
			
		||||
            // console.log('Selected Option: ', $("#subnet_ranges").select2('val'));
 | 
			
		||||
            updateIPAllocationSuggestion();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // apply_config_confirm button event
 | 
			
		||||
        $(document).ready(function () {
 | 
			
		||||
            $("#apply_config_confirm").click(function () {
 | 
			
		||||
                $.ajax({
 | 
			
		||||
                    cache: false,
 | 
			
		||||
                    method: 'GET',
 | 
			
		||||
                    url: '/api/apply-wg-config',
 | 
			
		||||
                    method: 'POST',
 | 
			
		||||
                    url: '{{.basePath}}/api/apply-wg-config',
 | 
			
		||||
                    dataType: 'json',
 | 
			
		||||
                    contentType: "application/json",
 | 
			
		||||
                    success: function(data) {
 | 
			
		||||
                        updateApplyConfigVisibility()
 | 
			
		||||
                        $("#modal_apply_config").modal('hide');
 | 
			
		||||
                        toastr.success('Applied config successfully');
 | 
			
		||||
                    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ Wireguard Clients
 | 
			
		|||
                <div class="modal-body">
 | 
			
		||||
                    <input type="hidden" id="e_client_id" name="e_client_id">
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="e_client_email" class="control-label">Email</label>
 | 
			
		||||
                        <label for="e_client_email" class="control-label">Email address</label>
 | 
			
		||||
                        <input type="text" class="form-control" id="e_client_email" name="e_client_email">
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -57,6 +57,57 @@ Wireguard Clients
 | 
			
		|||
    </div>
 | 
			
		||||
    <!-- /.modal-dialog -->
 | 
			
		||||
</div>
 | 
			
		||||
<!-- /.modal -->
 | 
			
		||||
 | 
			
		||||
<div class="modal fade" id="modal_qr_client">
 | 
			
		||||
    <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h4 class="modal-title">QR Code</h4>
 | 
			
		||||
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | 
			
		||||
                    <span aria-hidden="true">×</span>
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-body">
 | 
			
		||||
                <input type="hidden" id="qr_client_id" name="qr_client_id">
 | 
			
		||||
                <img id="qr_code" class="w-100" style="image-rendering: pixelated;" src="" alt="QR code" />
 | 
			
		||||
                <!-- do not include FwMark in any client configs: it is INVALID. -->
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- /.modal-content -->
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- /.modal-dialog -->
 | 
			
		||||
</div>
 | 
			
		||||
<!-- /.modal -->
 | 
			
		||||
 | 
			
		||||
<div class="modal fade" id="modal_telegram_client">
 | 
			
		||||
    <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h4 class="modal-title">Telegram Configuration</h4>
 | 
			
		||||
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | 
			
		||||
                    <span aria-hidden="true">×</span>
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <form name="frm_telegram_client" id="frm_telegram_client">
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                    <input type="hidden" id="tg_client_id" name="tg_client_id">
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="tg_client_userid" class="control-label">Telegram userid</label>
 | 
			
		||||
                        <input type="text" class="form-control" id="tg_client_userid" name="tg_client_userid">
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer justify-content-between">
 | 
			
		||||
                    <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
 | 
			
		||||
                    <button type="submit" class="btn btn-success">Send</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- /.modal-content -->
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- /.modal-dialog -->
 | 
			
		||||
</div>
 | 
			
		||||
<!-- /.modal -->
 | 
			
		||||
 | 
			
		||||
<div class="modal fade" id="modal_edit_client">
 | 
			
		||||
    <div class="modal-dialog">
 | 
			
		||||
| 
						 | 
				
			
			@ -78,6 +129,12 @@ Wireguard Clients
 | 
			
		|||
                        <label for="_client_email" class="control-label">Email</label>
 | 
			
		||||
                        <input type="text" class="form-control" id="_client_email" name="client_email">
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="_subnet_ranges" class="control-label">Subnet range</label>
 | 
			
		||||
                        <select id="_subnet_ranges" class="select2"
 | 
			
		||||
                            data-placeholder="Select a subnet range" style="width: 100%;">
 | 
			
		||||
                        </select>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="_client_allocated_ips" class="control-label">IP Allocation</label>
 | 
			
		||||
                        <input type="text" data-role="tagsinput" class="form-control" id="_client_allocated_ips">
 | 
			
		||||
| 
						 | 
				
			
			@ -86,6 +143,15 @@ Wireguard Clients
 | 
			
		|||
                        <label for="_client_allowed_ips" class="control-label">Allowed IPs</label>
 | 
			
		||||
                        <input type="text" data-role="tagsinput" class="form-control" id="_client_allowed_ips">
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="_client_extra_allowed_ips" class="control-label">Extra Allowed IPs</label>
 | 
			
		||||
                        <input type="text" data-role="tagsinput" class="form-control"
 | 
			
		||||
                               id="_client_extra_allowed_ips">
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="_client_endpoint" class="control-label">Endpoint</label>
 | 
			
		||||
                        <input type="text" class="form-control" id="_client_endpoint" name="client_endpoint">
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <div class="icheck-primary d-inline">
 | 
			
		||||
                            <input type="checkbox" id="_use_server_dns">
 | 
			
		||||
| 
						 | 
				
			
			@ -102,6 +168,38 @@ Wireguard Clients
 | 
			
		|||
                            </label>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <details>
 | 
			
		||||
                        <summary><strong>Public and Preshared Keys</strong>
 | 
			
		||||
                            <i class="fas fa-info-circle" data-toggle="tooltip"
 | 
			
		||||
                               data-original-title="Update the server stored
 | 
			
		||||
                               client Public and Preshared keys.">
 | 
			
		||||
                            </i>
 | 
			
		||||
                        </summary>
 | 
			
		||||
                        <div class="form-group" style="margin-top: 1rem">
 | 
			
		||||
                            <label for="_client_public_key" class="control-label">
 | 
			
		||||
                                Public Key
 | 
			
		||||
                            </label>
 | 
			
		||||
                            <input type="text" class="form-control" id="_client_public_key" name="_client_public_key" aria-invalid="false">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="_client_preshared_key" class="control-label">
 | 
			
		||||
                                Preshared Key
 | 
			
		||||
                            </label>
 | 
			
		||||
                            <input type="text" class="form-control" id="_client_preshared_key" name="_client_preshared_key">
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </details>
 | 
			
		||||
                    <details style="margin-top: 0.5rem;">
 | 
			
		||||
                        <summary><strong>Additional configuration</strong>
 | 
			
		||||
                        </summary>
 | 
			
		||||
                        <div class="form-group" style="margin-top: 0.5rem;">
 | 
			
		||||
                            <label for="_client_telegram_userid" class="control-label">Telegram userid</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="_client_telegram_userid" name="_client_telegram_userid">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="_additional_notes" class="control-label">Notes</label>
 | 
			
		||||
                            <textarea class="form-control" style="min-height: 6rem;" id="_additional_notes" name="_additional_notes" placeholder="Additional notes about this client"></textarea>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </details>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer justify-content-between">
 | 
			
		||||
                    <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
 | 
			
		||||
| 
						 | 
				
			
			@ -166,7 +264,7 @@ Wireguard Clients
 | 
			
		|||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'GET',
 | 
			
		||||
                url: '/api/clients',
 | 
			
		||||
                url: '{{.basePath}}/api/clients',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                success: function (data) {
 | 
			
		||||
| 
						 | 
				
			
			@ -184,7 +282,7 @@ Wireguard Clients
 | 
			
		|||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                url: '/client/set-status',
 | 
			
		||||
                url: '{{.basePath}}/client/set-status',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
| 
						 | 
				
			
			@ -202,20 +300,219 @@ Wireguard Clients
 | 
			
		|||
            setClientStatus(clientID, true);
 | 
			
		||||
            const divElement = document.getElementById("paused_" + clientID);
 | 
			
		||||
            divElement.style.visibility = "hidden";
 | 
			
		||||
            updateApplyConfigVisibility()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function pauseClient(clientID) {
 | 
			
		||||
            setClientStatus(clientID, false);
 | 
			
		||||
            const divElement = document.getElementById("paused_" + clientID);
 | 
			
		||||
            divElement.style.visibility = "visible";
 | 
			
		||||
            updateApplyConfigVisibility()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // updateIPAllocationSuggestion function for automatically fill
 | 
			
		||||
        // the IP Allocation input with suggested ip addresses
 | 
			
		||||
        // FOR CHANGING A SUBNET OF AN EXISTING CLIENT
 | 
			
		||||
        function updateIPAllocationSuggestionExisting() {
 | 
			
		||||
            let subnetRange = $("#_subnet_ranges").select2('val');
 | 
			
		||||
 | 
			
		||||
            if (!subnetRange || subnetRange.length === 0) {
 | 
			
		||||
                subnetRange = '__default_any__'
 | 
			
		||||
            }
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'GET',
 | 
			
		||||
                url: `{{.basePath}}/api/suggest-client-ips?sr=${subnetRange}`,
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                success: function(data) {
 | 
			
		||||
                    const allocated_ips = $("#_client_allocated_ips").val().split(",");
 | 
			
		||||
                    allocated_ips.forEach(function (item, index) {
 | 
			
		||||
                        $('#_client_allocated_ips').removeTag(escape(item));
 | 
			
		||||
                    })
 | 
			
		||||
                    data.forEach(function (item, index) {
 | 
			
		||||
                        $('#_client_allocated_ips').addTag(item);
 | 
			
		||||
                    })
 | 
			
		||||
                },
 | 
			
		||||
                error: function(jqXHR, exception) {
 | 
			
		||||
                    const allocated_ips = $("#_client_allocated_ips").val().split(",");
 | 
			
		||||
                    allocated_ips.forEach(function (item, index) {
 | 
			
		||||
                        $('#_client_allocated_ips').removeTag(escape(item));
 | 
			
		||||
                    })
 | 
			
		||||
                    const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                    toastr.error(responseJson['message']);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function updateSubnetRangesList(elementID, preselectedVal) {
 | 
			
		||||
            $.getJSON("{{.basePath}}/api/subnet-ranges", null, function(data) {
 | 
			
		||||
                $(`${elementID} option`).remove();
 | 
			
		||||
                $(elementID).append(
 | 
			
		||||
                    $("<option></option>")
 | 
			
		||||
                        .text("Any")
 | 
			
		||||
                        .val("__default_any__")
 | 
			
		||||
                );
 | 
			
		||||
                $.each(data, function(index, item) {
 | 
			
		||||
                    $(elementID).append(
 | 
			
		||||
                        $("<option></option>")
 | 
			
		||||
                            .text(item)
 | 
			
		||||
                            .val(item)
 | 
			
		||||
                    );
 | 
			
		||||
                    if (item === preselectedVal) {
 | 
			
		||||
                        console.log(preselectedVal);
 | 
			
		||||
                        $(elementID).val(preselectedVal).trigger('change')
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function updateSearchList() {
 | 
			
		||||
            $.getJSON("{{.basePath}}/api/subnet-ranges", null, function(data) {
 | 
			
		||||
                $("#status-selector option").remove();
 | 
			
		||||
                $("#status-selector").append(
 | 
			
		||||
                    $("<option></option>")
 | 
			
		||||
                        .text("All")
 | 
			
		||||
                        .val("All"),
 | 
			
		||||
                    $("<option></option>")
 | 
			
		||||
                        .text("Enabled")
 | 
			
		||||
                        .val("Enabled"),
 | 
			
		||||
                    $("<option></option>")
 | 
			
		||||
                        .text("Disabled")
 | 
			
		||||
                        .val("Disabled"),
 | 
			
		||||
                    $("<option></option>")
 | 
			
		||||
                        .text("Connected")
 | 
			
		||||
                        .val("Connected"),
 | 
			
		||||
                    $("<option></option>")
 | 
			
		||||
                        .text("Disconnected")
 | 
			
		||||
                        .val("Disconnected")
 | 
			
		||||
                );
 | 
			
		||||
                $.each(data, function(index, item) {
 | 
			
		||||
                    $("#status-selector").append(
 | 
			
		||||
                        $("<option></option>")
 | 
			
		||||
                            .text(item)
 | 
			
		||||
                            .val(item)
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    </script>
 | 
			
		||||
    <script>
 | 
			
		||||
        // load client list
 | 
			
		||||
        $(document).ready(function () {
 | 
			
		||||
            updateSearchList();
 | 
			
		||||
            populateClientList();
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // show search bar and override :contains to be case-insensitive
 | 
			
		||||
        $(document).ready(function () {
 | 
			
		||||
            $("#search-form").show();
 | 
			
		||||
            jQuery.expr[':'].contains = function(a, i, m) {
 | 
			
		||||
                return jQuery(a).text().toUpperCase()
 | 
			
		||||
                    .indexOf(m[3].toUpperCase()) >= 0;
 | 
			
		||||
            };
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // hide all clients and display only the ones that meet the search criteria (name, email, IP)
 | 
			
		||||
        $('#search-input').keyup(function () {
 | 
			
		||||
            $("#status-selector").val("All");
 | 
			
		||||
            let query = $(this).val().trim();
 | 
			
		||||
            $('.col-lg-4').hide();
 | 
			
		||||
            $(".info-box-text").each(function() {
 | 
			
		||||
                if($(this).children('i.fa-user').length > 0 || $(this).children('i.fa-envelope').length > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    $(this).filter(':contains("' + query + '")').parent().parent().parent().show();
 | 
			
		||||
                }
 | 
			
		||||
                })
 | 
			
		||||
            $(".badge-secondary").filter(':contains("' + query + '")').parent().parent().parent().show();
 | 
			
		||||
            $(".fa-tguserid").each(function () {
 | 
			
		||||
                if ($(this).parent().text().trim().indexOf(query) != -1) {
 | 
			
		||||
                    $(this).closest('.col-lg-4').show();
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            let upperQuery = query.toUpperCase()
 | 
			
		||||
            $(".fa-additional_notes").each(function () {
 | 
			
		||||
                if ($(this).parent().text().trim().indexOf(upperQuery) != -1) {
 | 
			
		||||
                    $(this).closest('.col-lg-4').show();
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        $("#status-selector").on('change', function () {
 | 
			
		||||
            $('#search-input').val("");
 | 
			
		||||
            switch ($("#status-selector").val()) {
 | 
			
		||||
                case "All":
 | 
			
		||||
                    $('.col-lg-4').show();
 | 
			
		||||
                    break;
 | 
			
		||||
                case "Enabled":
 | 
			
		||||
                    $('.col-lg-4').hide();
 | 
			
		||||
                    $('[id^="paused_"]').each(function () {
 | 
			
		||||
                        if ($(this).css("visibility") === "hidden") {
 | 
			
		||||
                            $(this).parent().parent().show();
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                    break;
 | 
			
		||||
                case "Disabled":
 | 
			
		||||
                    $('.col-lg-4').hide();
 | 
			
		||||
                    $('[id^="paused_"]').each(function () {
 | 
			
		||||
                        if ($(this).css("visibility") !== "hidden") {
 | 
			
		||||
                            $(this).parent().parent().show();
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                    break;
 | 
			
		||||
                case "Connected":
 | 
			
		||||
                    $('.col-lg-4').hide();
 | 
			
		||||
                    $.ajax({
 | 
			
		||||
                        cache: false,
 | 
			
		||||
                        method: 'GET',
 | 
			
		||||
                        url: '{{.basePath}}/status',
 | 
			
		||||
                        success: function (data) {
 | 
			
		||||
                            const returnedHTML = $(data).find(".table-success").get();
 | 
			
		||||
                            var returnedString = "";
 | 
			
		||||
                            returnedHTML.forEach(entry => returnedString += entry.outerHTML);
 | 
			
		||||
                            $(".fa-key").each(function () {
 | 
			
		||||
                                if (returnedString.indexOf($(this).parent().text().trim()) != -1) {
 | 
			
		||||
                                    $(this).closest('.col-lg-4').show();
 | 
			
		||||
                                }
 | 
			
		||||
                            })
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                    break;
 | 
			
		||||
                case "Disconnected":
 | 
			
		||||
                    $('.col-lg-4').show();
 | 
			
		||||
                    $.ajax({
 | 
			
		||||
                        cache: false,
 | 
			
		||||
                        method: 'GET',
 | 
			
		||||
                        url: '{{.basePath}}/status',
 | 
			
		||||
                        success: function (data) {
 | 
			
		||||
                            const returnedHTML = $(data).find(".table-success").get();
 | 
			
		||||
                            var returnedString = "";
 | 
			
		||||
                            returnedHTML.forEach(entry => returnedString += entry.outerHTML);
 | 
			
		||||
                            $(".fa-key").each(function () {
 | 
			
		||||
                                if (returnedString.indexOf($(this).parent().text().trim()) != -1) {
 | 
			
		||||
                                    $(this).closest('.col-lg-4').hide();
 | 
			
		||||
                                }
 | 
			
		||||
                            })
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    $('.col-lg-4').hide();
 | 
			
		||||
                    const selectedSR = $("#status-selector").val()
 | 
			
		||||
                    $(".fa-subnetrange").each(function () {
 | 
			
		||||
                        const srs = $(this).parent().text().trim().split(',')
 | 
			
		||||
                        for (const sr of srs) {
 | 
			
		||||
                            if (sr === selectedSR) {
 | 
			
		||||
                                $(this).closest('.col-lg-4').show();
 | 
			
		||||
                                break
 | 
			
		||||
                            }                            
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                    // $('.col-lg-4').show();
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // modal_pause_client modal event
 | 
			
		||||
        $("#modal_pause_client").on('show.bs.modal', function (event) {
 | 
			
		||||
            const button = $(event.relatedTarget);
 | 
			
		||||
| 
						 | 
				
			
			@ -253,7 +550,7 @@ Wireguard Clients
 | 
			
		|||
                $.ajax({
 | 
			
		||||
                    cache: false,
 | 
			
		||||
                    method: 'POST',
 | 
			
		||||
                    url: '/remove-client',
 | 
			
		||||
                    url: '{{.basePath}}/remove-client',
 | 
			
		||||
                    dataType: 'json',
 | 
			
		||||
                    contentType: "application/json",
 | 
			
		||||
                    data: JSON.stringify(data),
 | 
			
		||||
| 
						 | 
				
			
			@ -262,6 +559,7 @@ Wireguard Clients
 | 
			
		|||
                        toastr.success('Removed client successfully');
 | 
			
		||||
                        const divElement = document.getElementById('client_' + client_id);
 | 
			
		||||
                        divElement.style.display = "none";
 | 
			
		||||
                        updateApplyConfigVisibility()
 | 
			
		||||
                    },
 | 
			
		||||
                    error: function(jqXHR, exception) {
 | 
			
		||||
                        const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
| 
						 | 
				
			
			@ -274,8 +572,9 @@ Wireguard Clients
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
        // Edit client modal event
 | 
			
		||||
        // This fills the modal dialogue with data from the DB when we open the edit menu
 | 
			
		||||
        $(document).ready(function () {
 | 
			
		||||
            $("#modal_edit_client").on('shown.bs.modal', function (event) {
 | 
			
		||||
            $("#modal_edit_client").on('show.bs.modal', function (event) {
 | 
			
		||||
                let modal = $(this);
 | 
			
		||||
                const button = $(event.relatedTarget);
 | 
			
		||||
                const client_id = button.data('clientid');
 | 
			
		||||
| 
						 | 
				
			
			@ -288,6 +587,7 @@ Wireguard Clients
 | 
			
		|||
                    'defaultText': 'Add More',
 | 
			
		||||
                    'removeWithBackspace': true,
 | 
			
		||||
                    'minChars': 0,
 | 
			
		||||
                    'minInputWidth': '100%',
 | 
			
		||||
                    'placeholderColor': '#666666'
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -299,14 +599,26 @@ Wireguard Clients
 | 
			
		|||
                    'defaultText': 'Add More',
 | 
			
		||||
                    'removeWithBackspace': true,
 | 
			
		||||
                    'minChars': 0,
 | 
			
		||||
                    'minInputWidth': '100%',
 | 
			
		||||
                    'placeholderColor': '#666666'
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                modal.find("#_client_extra_allowed_ips").tagsInput({
 | 
			
		||||
                    'width': '100%',
 | 
			
		||||
                    'height': '75%',
 | 
			
		||||
                    'interactive': true,
 | 
			
		||||
                    'defaultText': 'Add More',
 | 
			
		||||
                    'removeWithBackspace' : true,
 | 
			
		||||
                    'minChars': 0,
 | 
			
		||||
                    'minInputWidth': '100%',
 | 
			
		||||
                    'placeholderColor': '#666666'
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
                // update client modal data
 | 
			
		||||
                $.ajax({
 | 
			
		||||
                    cache: false,
 | 
			
		||||
                    method: 'GET',
 | 
			
		||||
                    url: '/api/client/' + client_id,
 | 
			
		||||
                    url: '{{.basePath}}/api/client/' + client_id,
 | 
			
		||||
                    dataType: 'json',
 | 
			
		||||
                    contentType: "application/json",
 | 
			
		||||
                    success: function (resp) {
 | 
			
		||||
| 
						 | 
				
			
			@ -314,9 +626,17 @@ Wireguard Clients
 | 
			
		|||
 | 
			
		||||
                        modal.find(".modal-title").text("Edit Client " + client.name);
 | 
			
		||||
                        modal.find("#_client_id").val(client.id);
 | 
			
		||||
                        modal.find("#_client_telegram_userid").val(client.telegram_userid);
 | 
			
		||||
                        modal.find("#_client_name").val(client.name);
 | 
			
		||||
                        modal.find("#_client_email").val(client.email);
 | 
			
		||||
 | 
			
		||||
                        let preselectedEl
 | 
			
		||||
                        if (client.subnet_ranges && client.subnet_ranges.length > 0) {
 | 
			
		||||
                            preselectedEl = client.subnet_ranges[0]
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        updateSubnetRangesList("#_subnet_ranges", preselectedEl);
 | 
			
		||||
 | 
			
		||||
                        modal.find("#_client_allocated_ips").importTags('');
 | 
			
		||||
                        client.allocated_ips.forEach(function (obj) {
 | 
			
		||||
                            modal.find("#_client_allocated_ips").addTag(obj);
 | 
			
		||||
| 
						 | 
				
			
			@ -327,8 +647,25 @@ Wireguard Clients
 | 
			
		|||
                            modal.find("#_client_allowed_ips").addTag(obj);
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        modal.find("#_client_extra_allowed_ips").importTags('');
 | 
			
		||||
                        client.extra_allowed_ips.forEach(function (obj) {
 | 
			
		||||
                            modal.find("#_client_extra_allowed_ips").addTag(obj);
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        modal.find("#_client_endpoint").val(client.endpoint);
 | 
			
		||||
 | 
			
		||||
                        modal.find("#_use_server_dns").prop("checked", client.use_server_dns);
 | 
			
		||||
                        modal.find("#_enabled").prop("checked", client.enabled);
 | 
			
		||||
 | 
			
		||||
                        modal.find("#_client_public_key").val(client.public_key);
 | 
			
		||||
                        modal.find("#_client_preshared_key").val(client.preshared_key);
 | 
			
		||||
                        
 | 
			
		||||
                        modal.find("#_additional_notes").val(client.additional_notes);
 | 
			
		||||
 | 
			
		||||
                        // handle subnet range select
 | 
			
		||||
                        $('#_subnet_ranges').on('select2:select', function (e) {
 | 
			
		||||
                            updateIPAllocationSuggestionExisting();
 | 
			
		||||
                        });
 | 
			
		||||
                    },
 | 
			
		||||
                    error: function (jqXHR, exception) {
 | 
			
		||||
                        const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
| 
						 | 
				
			
			@ -338,7 +675,37 @@ Wireguard Clients
 | 
			
		|||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // submitEmailClient function for sending an email to the client with the configuration
 | 
			
		||||
        // regenerateQRCode function for regenerating QR Code adding/removing some parts of configuration because of compatibility issues with some clients
 | 
			
		||||
        function regenerateQRCode() {
 | 
			
		||||
            const client_id = $("#qr_client_id").val();
 | 
			
		||||
            const QRCodeImg = $("#qr_code");
 | 
			
		||||
            const QRCodeA = $("#qr_code_a");
 | 
			
		||||
            QRCodeImg.hide();
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'GET',
 | 
			
		||||
                url: '{{.basePath}}/api/client/' + client_id,
 | 
			
		||||
                data: {
 | 
			
		||||
 | 
			
		||||
                },
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                success: function (resp) {
 | 
			
		||||
                    const client = resp.Client;
 | 
			
		||||
 | 
			
		||||
                    $(".modal-title").text("Scan QR Code for " + client.name + " profile");
 | 
			
		||||
                    QRCodeImg.attr('src', resp.QRCode).show();
 | 
			
		||||
                    QRCodeA.attr('download', resp.Client.name);
 | 
			
		||||
                    QRCodeA.attr('href', resp.QRCode).show();
 | 
			
		||||
                },
 | 
			
		||||
                error: function (jqXHR, exception) {
 | 
			
		||||
                    const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                    toastr.error(responseJson['message']);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // submitEmailClient function for sending an email with the configuration to the client
 | 
			
		||||
        function submitEmailClient() {
 | 
			
		||||
            const client_id = $("#e_client_id").val();
 | 
			
		||||
            const email = $("#e_client_email").val();
 | 
			
		||||
| 
						 | 
				
			
			@ -346,15 +713,36 @@ Wireguard Clients
 | 
			
		|||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                url: '/email-client',
 | 
			
		||||
                url: '{{.basePath}}/email-client',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
                success: function(resp) {
 | 
			
		||||
                    $("#modal_email_client").modal('hide');
 | 
			
		||||
                    toastr.success('Sent email to client successfully');
 | 
			
		||||
                     // Refresh the home page (clients page) after sending email successfully
 | 
			
		||||
                    location.reload();
 | 
			
		||||
                },
 | 
			
		||||
                error: function(jqXHR, exception) {
 | 
			
		||||
                    const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                    toastr.error(responseJson['message']);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // submitTelegramClient function for sending a telegram message with the configuration to the client
 | 
			
		||||
        function submitTelegramClient() {
 | 
			
		||||
            const client_id = $("#tg_client_id").val();
 | 
			
		||||
            const userid = $("#tg_client_userid").val();
 | 
			
		||||
            const data = {"id": client_id, "userid": userid};
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                url: '{{.basePath}}/send-telegram-client',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
                success: function(resp) {
 | 
			
		||||
                    $("#modal_telegram_client").modal('hide');
 | 
			
		||||
                    toastr.success('Sent config via telegram to client successfully');
 | 
			
		||||
                },
 | 
			
		||||
                error: function(jqXHR, exception) {
 | 
			
		||||
                    const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
| 
						 | 
				
			
			@ -364,13 +752,25 @@ Wireguard Clients
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        // submitEditClient function for updating an existing client
 | 
			
		||||
        // This sends dialogue data to the back-end when user presses "Save"
 | 
			
		||||
        // See e.g. routes.go:UpdateClient for where data is processed/verified.
 | 
			
		||||
        function submitEditClient() {
 | 
			
		||||
            const client_id = $("#_client_id").val();
 | 
			
		||||
            const name = $("#_client_name").val();
 | 
			
		||||
            const email = $("#_client_email").val();
 | 
			
		||||
            const telegram_userid = $("#_client_telegram_userid").val();
 | 
			
		||||
            const allocated_ips = $("#_client_allocated_ips").val().split(",");
 | 
			
		||||
            const allowed_ips = $("#_client_allowed_ips").val().split(",");
 | 
			
		||||
            let use_server_dns = false;
 | 
			
		||||
            let extra_allowed_ips = [];
 | 
			
		||||
            const public_key = $("#_client_public_key").val();
 | 
			
		||||
            const preshared_key = $("#_client_preshared_key").val();
 | 
			
		||||
 | 
			
		||||
            if( $("#_client_extra_allowed_ips").val() !== "" ) {
 | 
			
		||||
                extra_allowed_ips = $("#_client_extra_allowed_ips").val().split(",");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const endpoint = $("#_client_endpoint").val();
 | 
			
		||||
 | 
			
		||||
            if ($("#_use_server_dns").is(':checked')){
 | 
			
		||||
                use_server_dns = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -382,13 +782,16 @@ Wireguard Clients
 | 
			
		|||
                enabled = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const data = {"id": client_id, "name": name, "email": email, "allocated_ips": allocated_ips,
 | 
			
		||||
                "allowed_ips": allowed_ips, "use_server_dns": use_server_dns, "enabled": enabled};
 | 
			
		||||
            const additional_notes = $("#_additional_notes").val();
 | 
			
		||||
 | 
			
		||||
            const data = {"id": client_id, "name": name, "email": email, "telegram_userid": telegram_userid, "allocated_ips": allocated_ips,
 | 
			
		||||
                "allowed_ips": allowed_ips, "extra_allowed_ips": extra_allowed_ips, "endpoint": endpoint,
 | 
			
		||||
                "use_server_dns": use_server_dns, "enabled": enabled, "public_key": public_key, "preshared_key": preshared_key, "additional_notes": additional_notes};
 | 
			
		||||
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                url: '/update-client',
 | 
			
		||||
                url: '{{.basePath}}/update-client',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
| 
						 | 
				
			
			@ -410,25 +813,27 @@ Wireguard Clients
 | 
			
		|||
            const formId = $(form).attr('id');
 | 
			
		||||
            if (formId === "frm_edit_client") {
 | 
			
		||||
                submitEditClient();
 | 
			
		||||
            }else if (formId === "frm_email_client") {
 | 
			
		||||
            } else if (formId === "frm_email_client") {
 | 
			
		||||
                submitEmailClient();
 | 
			
		||||
            } else if (formId === "frm_telegram_client") {
 | 
			
		||||
                submitTelegramClient();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $("#modal_email_client").on('shown.bs.modal', function (event) {
 | 
			
		||||
        $("#modal_email_client").on('show.bs.modal', function (event) {
 | 
			
		||||
            let modal = $(this);
 | 
			
		||||
            const button = $(event.relatedTarget);
 | 
			
		||||
            const client_id = button.data('clientid');
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'GET',
 | 
			
		||||
                url: '/api/client/' + client_id,
 | 
			
		||||
                url: '{{.basePath}}/api/client/' + client_id,
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                success: function (resp) {
 | 
			
		||||
                    const client = resp.Client;
 | 
			
		||||
 | 
			
		||||
                    modal.find(".modal-title").text("Email Client " + client.name);
 | 
			
		||||
                    modal.find(".modal-title").text("Send config to client " + client.name);
 | 
			
		||||
                    modal.find("#e_client_id").val(client.id);
 | 
			
		||||
                    modal.find("#e_client_email").val(client.email);
 | 
			
		||||
                },
 | 
			
		||||
| 
						 | 
				
			
			@ -439,10 +844,42 @@ Wireguard Clients
 | 
			
		|||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $("#modal_qr_client").on('show.bs.modal', function (event) {
 | 
			
		||||
            let modal = $(this);
 | 
			
		||||
            const button = $(event.relatedTarget);
 | 
			
		||||
            const client_id = button.data('clientid');
 | 
			
		||||
 | 
			
		||||
            modal.find("#qr_client_id").val(client_id);
 | 
			
		||||
            regenerateQRCode();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $("#modal_telegram_client").on('show.bs.modal', function (event) {
 | 
			
		||||
            let modal = $(this);
 | 
			
		||||
            const button = $(event.relatedTarget);
 | 
			
		||||
            const client_id = button.data('clientid');
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'GET',
 | 
			
		||||
                url: '{{.basePath}}/api/client/' + client_id,
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                success: function (resp) {
 | 
			
		||||
                    const client = resp.Client;
 | 
			
		||||
 | 
			
		||||
                    modal.find(".modal-title").text("Send config to client " + client.name);
 | 
			
		||||
                    modal.find("#tg_client_id").val(client.id);
 | 
			
		||||
                    modal.find("#tg_client_userid").val(client.telegram_userid);
 | 
			
		||||
                },
 | 
			
		||||
                error: function (jqXHR, exception) {
 | 
			
		||||
                    const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                    toastr.error(responseJson['message']);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $(document).ready(function () {
 | 
			
		||||
            $.validator.setDefaults({
 | 
			
		||||
                submitHandler: function (form) {
 | 
			
		||||
                    //submitEditClient();
 | 
			
		||||
                    submitHandler(form);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
| 
						 | 
				
			
			@ -495,6 +932,33 @@ Wireguard Clients
 | 
			
		|||
                    $(element).removeClass('is-invalid');
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            // Telegram client form validation
 | 
			
		||||
            $("#frm_telegram_client").validate({
 | 
			
		||||
                rules: {
 | 
			
		||||
                    tg_client_userid: {
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        number: true,
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
                messages: {
 | 
			
		||||
                    tg_client_userid: {
 | 
			
		||||
                        required: "Please enter a telegram userid",
 | 
			
		||||
                        number: "Please enter a valid telegram userid"
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
                errorElement: 'span',
 | 
			
		||||
                errorPlacement: function (error, element) {
 | 
			
		||||
                    error.addClass('invalid-feedback');
 | 
			
		||||
                    element.closest('.form-group').append(error);
 | 
			
		||||
                },
 | 
			
		||||
                highlight: function (element, errorClass, validClass) {
 | 
			
		||||
                    $(element).addClass('is-invalid');
 | 
			
		||||
                },
 | 
			
		||||
                unhighlight: function (element, errorClass, validClass) {
 | 
			
		||||
                    $(element).removeClass('is-invalid');
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            // 
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
{{end}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,6 +55,18 @@ Global Settings
 | 
			
		|||
                                    name="persistent_keepalive" placeholder="Persistent Keepalive"
 | 
			
		||||
                                    value="{{if .globalSettings.PersistentKeepalive }}{{ .globalSettings.PersistentKeepalive }}{{end}}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label for="firewall_mark">Firewall Mark</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="firewall_mark"
 | 
			
		||||
                                    name="firewall_mark" placeholder="Firewall Mark"
 | 
			
		||||
                                    value="{{ .globalSettings.FirewallMark }}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label for="Table">Table</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="table"
 | 
			
		||||
                                    name="table" placeholder="auto"
 | 
			
		||||
                                    value="{{ .globalSettings.Table }}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label for="config_file_path">Wireguard Config File Path</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="config_file_path"
 | 
			
		||||
| 
						 | 
				
			
			@ -85,16 +97,20 @@ Global Settings
 | 
			
		|||
                            <dt>2. DNS Servers</dt>
 | 
			
		||||
                            <dd>The DNS servers will be set to client config.</dd>
 | 
			
		||||
                            <dt>3. MTU</dt>
 | 
			
		||||
                            <dd>The MTU will be set to server config. By default it is <code>1420</code>. You might want
 | 
			
		||||
                            <dd>The MTU will be set to server and client config. By default it is <code>1450</code>. You might want
 | 
			
		||||
                                to adjust the MTU size if your connection (e.g PPPoE, 3G, satellite network, etc) has a low MTU.</dd>
 | 
			
		||||
                            <dd>Leave blank to omit this setting in the Server config.</dd>
 | 
			
		||||
                            <dd>Leave blank to omit this setting in the configs.</dd>
 | 
			
		||||
                            <dt>4. Persistent Keepalive</dt>
 | 
			
		||||
                            <dd>By default, WireGuard peers remain silent while they do not need to communicate,
 | 
			
		||||
                                so peers located behind a NAT and/or firewall may be unreachable from other peers
 | 
			
		||||
                                until they reach out to other peers themselves. Adding <code>PersistentKeepalive</code>
 | 
			
		||||
                                can ensure that the connection remains open.</dd>
 | 
			
		||||
                            <dd>Leave blank to omit this setting in the Client config.</dd>
 | 
			
		||||
                            <dt>5. Wireguard Config File Path</dt>
 | 
			
		||||
                            <dt>5. Firewall Mark</dt>
 | 
			
		||||
                            <dd>Add a matching <code>fwmark</code> on all packets going out of a WireGuard non-default-route tunnel. Default value: <code>0xca6c</code></dd>
 | 
			
		||||
                            <dt>6. Table</dt>
 | 
			
		||||
                            <dd>Value for the <code>Table</code> setting in the wg conf file. Default value: <code>auto</code></dd>                            
 | 
			
		||||
                            <dt>7. Wireguard Config File Path</dt>
 | 
			
		||||
                            <dd>The path of your Wireguard server config file. Please make sure the parent directory
 | 
			
		||||
                                exists and is writable.</dd>
 | 
			
		||||
                        </dl>
 | 
			
		||||
| 
						 | 
				
			
			@ -141,13 +157,15 @@ Global Settings
 | 
			
		|||
            const dns_servers = $("#dns_servers").val().split(",");
 | 
			
		||||
            const mtu = $("#mtu").val();
 | 
			
		||||
            const persistent_keepalive = $("#persistent_keepalive").val();
 | 
			
		||||
            const firewall_mark = $("#firewall_mark").val();
 | 
			
		||||
            const table = $("#table").val();
 | 
			
		||||
            const config_file_path = $("#config_file_path").val();
 | 
			
		||||
            const data = {"endpoint_address": endpoint_address, "dns_servers": dns_servers, "mtu": mtu, "persistent_keepalive": persistent_keepalive, "config_file_path": config_file_path};
 | 
			
		||||
            const data = {"endpoint_address": endpoint_address, "dns_servers": dns_servers, "mtu": mtu, "persistent_keepalive": persistent_keepalive, "firewall_mark": firewall_mark, "table": table, "config_file_path": config_file_path};
 | 
			
		||||
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                url: '/global-settings',
 | 
			
		||||
                url: '{{.basePath}}/global-settings',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
| 
						 | 
				
			
			@ -163,7 +181,7 @@ Global Settings
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        function updateEndpointSuggestionIP() {
 | 
			
		||||
            $.getJSON("/api/machine-ips", null, function(data) {
 | 
			
		||||
            $.getJSON("{{.basePath}}/api/machine-ips", null, function(data) {
 | 
			
		||||
                $("#ip_suggestion option").remove();
 | 
			
		||||
                $.each(data, function(index, item) {
 | 
			
		||||
                    $("#ip_suggestion").append(
 | 
			
		||||
| 
						 | 
				
			
			@ -185,11 +203,13 @@ Global Settings
 | 
			
		|||
            'defaultText': 'Add More',
 | 
			
		||||
            'removeWithBackspace': true,
 | 
			
		||||
            'minChars': 0,
 | 
			
		||||
            'minInputWidth': '100%',
 | 
			
		||||
            'placeholderColor': '#666666'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Load DNS server to the form
 | 
			
		||||
        {{range .globalSettings.DNSServers}}
 | 
			
		||||
        $("#dns_servers").removeTag('{{.}}');
 | 
			
		||||
        $("#dns_servers").addTag('{{.}}');
 | 
			
		||||
        {{end}}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -211,6 +231,12 @@ Global Settings
 | 
			
		|||
                    },
 | 
			
		||||
                    config_file_path: {
 | 
			
		||||
                        required: true
 | 
			
		||||
                    },
 | 
			
		||||
                    firewall_mark: {
 | 
			
		||||
                        required: false
 | 
			
		||||
                    },
 | 
			
		||||
                    table: {
 | 
			
		||||
                        required: false
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                messages: {
 | 
			
		||||
| 
						 | 
				
			
			@ -255,4 +281,4 @@ Global Settings
 | 
			
		|||
            });
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{end}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,15 +7,17 @@
 | 
			
		|||
    <title>WireGuard UI</title>
 | 
			
		||||
    <!-- Tell the browser to be responsive to screen width -->
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
    <!-- Favicon -->
 | 
			
		||||
    <link rel="icon" href="{{.basePath}}/favicon">
 | 
			
		||||
 | 
			
		||||
    <!-- Font Awesome -->
 | 
			
		||||
    <link rel="stylesheet" href="static/plugins/fontawesome-free/css/all.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="{{.basePath}}/static/plugins/fontawesome-free/css/all.min.css">
 | 
			
		||||
    <!-- Ionicons -->
 | 
			
		||||
    <link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
 | 
			
		||||
    <!-- icheck bootstrap -->
 | 
			
		||||
    <link rel="stylesheet" href="static/plugins/icheck-bootstrap/icheck-bootstrap.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="{{.basePath}}/static/plugins/icheck-bootstrap/icheck-bootstrap.min.css">
 | 
			
		||||
    <!-- Theme style -->
 | 
			
		||||
    <link rel="stylesheet" href="static/dist/css/adminlte.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="{{.basePath}}/static/dist/css/adminlte.min.css">
 | 
			
		||||
    <!-- Google Font: Source Sans Pro -->
 | 
			
		||||
    <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet">
 | 
			
		||||
</head>
 | 
			
		||||
| 
						 | 
				
			
			@ -71,21 +73,21 @@
 | 
			
		|||
    </div>
 | 
			
		||||
    <!-- /.login-box -->
 | 
			
		||||
    <!-- jQuery -->
 | 
			
		||||
    <script src="static/plugins/jquery/jquery.min.js"></script>
 | 
			
		||||
    <script src="{{.basePath}}/static/plugins/jquery/jquery.min.js"></script>
 | 
			
		||||
    <!-- Bootstrap 4 -->
 | 
			
		||||
    <script src="static/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
 | 
			
		||||
    <script src="{{.basePath}}/static/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
 | 
			
		||||
    <!-- AdminLTE App -->
 | 
			
		||||
    <script src="static/dist/js/adminlte.min.js"></script>
 | 
			
		||||
    <script src="{{.basePath}}/static/dist/js/adminlte.min.js"></script>
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
<script>
 | 
			
		||||
    function redirectNext() {
 | 
			
		||||
        const urlParams = new URLSearchParams(window.location.search);
 | 
			
		||||
        const nextURL = urlParams.get('next');
 | 
			
		||||
        if (nextURL) {
 | 
			
		||||
        if (nextURL && /(?:^\/[a-zA-Z_])|(?:^\/$)/.test(nextURL.trim())) {
 | 
			
		||||
            window.location.href = nextURL;
 | 
			
		||||
        } else {
 | 
			
		||||
            window.location.href = '/';
 | 
			
		||||
            window.location.href = '/{{.basePath}}';
 | 
			
		||||
        } 
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -99,13 +101,16 @@
 | 
			
		|||
        $("#btn_login").click(function () {
 | 
			
		||||
            const username = $("#username").val();
 | 
			
		||||
            const password = $("#password").val();
 | 
			
		||||
            const data = {"username": username, "password": password}
 | 
			
		||||
            console.log(data);
 | 
			
		||||
            let rememberMe = false;
 | 
			
		||||
            if ($("#remember").is(':checked')){
 | 
			
		||||
                rememberMe = true;
 | 
			
		||||
            }
 | 
			
		||||
            const data = {"username": username, "password": password, "rememberMe": rememberMe}
 | 
			
		||||
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                url: '/login',
 | 
			
		||||
                url: '{{.basePath}}/login',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,136 @@
 | 
			
		|||
{{ define "title"}}
 | 
			
		||||
Profile
 | 
			
		||||
{{ end }}
 | 
			
		||||
 | 
			
		||||
{{ define "top_css"}}
 | 
			
		||||
{{ end }}
 | 
			
		||||
 | 
			
		||||
{{ define "username"}}
 | 
			
		||||
{{ .username }}
 | 
			
		||||
{{ end }}
 | 
			
		||||
 | 
			
		||||
{{ define "page_title"}}
 | 
			
		||||
Profile
 | 
			
		||||
{{ end }}
 | 
			
		||||
 | 
			
		||||
{{ define "page_content"}}
 | 
			
		||||
<section class="content">
 | 
			
		||||
    <div class="container-fluid">
 | 
			
		||||
        <!-- <h5 class="mt-4 mb-2">Global Settings</h5> -->
 | 
			
		||||
        <div class="row">
 | 
			
		||||
            <!-- left column -->
 | 
			
		||||
            <div class="col-md-6">
 | 
			
		||||
                <div class="card card-success">
 | 
			
		||||
                    <div class="card-header">
 | 
			
		||||
                        <h3 class="card-title">Update user information</h3>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <!-- /.card-header -->
 | 
			
		||||
                    <!-- form start -->
 | 
			
		||||
                    <form role="form" id="frm_profile" name="frm_profile">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label for="username" class="control-label">Username</label>
 | 
			
		||||
                                <input type="text" class="form-control" name="username" id="username"
 | 
			
		||||
                                       value="">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label for="password" class="control-label">Password</label>
 | 
			
		||||
                                <input type="password" class="form-control" name="password" id="password"
 | 
			
		||||
                                       value="" placeholder="Leave empty to keep the password unchanged">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <!-- /.card-body -->
 | 
			
		||||
                            <div class="card-footer">
 | 
			
		||||
                                <button type="submit" class="btn btn-success" id="update">Update</button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!-- /.card -->
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- /.row -->
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
{{ end }}
 | 
			
		||||
 | 
			
		||||
{{ define "bottom_js"}}
 | 
			
		||||
<script>
 | 
			
		||||
    {
 | 
			
		||||
        var previous_username;
 | 
			
		||||
        var admin;
 | 
			
		||||
    }
 | 
			
		||||
    $(document).ready(function () {
 | 
			
		||||
        $.ajax({
 | 
			
		||||
            cache: false,
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
            url: '{{.basePath}}/api/user/{{.baseData.CurrentUser}}',
 | 
			
		||||
            dataType: 'json',
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            success: function (resp) {
 | 
			
		||||
                const user = resp;
 | 
			
		||||
                $("#username").val(user.username);
 | 
			
		||||
                previous_username = user.username;
 | 
			
		||||
                admin = user.admin;
 | 
			
		||||
            },
 | 
			
		||||
            error: function (jqXHR, exception) {
 | 
			
		||||
                const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                toastr.error(responseJson['message']);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    function updateUserInfo() {
 | 
			
		||||
        const username = $("#username").val();
 | 
			
		||||
        const password = $("#password").val();
 | 
			
		||||
        const data = {"username": username, "password": password, "previous_username": previous_username, "admin":admin};
 | 
			
		||||
        $.ajax({
 | 
			
		||||
            cache: false,
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            url: '{{.basePath}}/update-user',
 | 
			
		||||
            dataType: 'json',
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            data: JSON.stringify(data),
 | 
			
		||||
            success: function (data) {
 | 
			
		||||
                toastr.success("Updated user information successfully");
 | 
			
		||||
                location.reload();
 | 
			
		||||
            },
 | 
			
		||||
            error: function (jqXHR, exception) {
 | 
			
		||||
                const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                toastr.error(responseJson['message']);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $(document).ready(function () {
 | 
			
		||||
        $.validator.setDefaults({
 | 
			
		||||
            submitHandler: function () {
 | 
			
		||||
                updateUserInfo();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        $("#frm_profile").validate({
 | 
			
		||||
            rules: {
 | 
			
		||||
                username: {
 | 
			
		||||
                    required: true
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            messages: {
 | 
			
		||||
                username: {
 | 
			
		||||
                    required: "Please enter a username",
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            errorElement: 'span',
 | 
			
		||||
            errorPlacement: function (error, element) {
 | 
			
		||||
                error.addClass('invalid-feedback');
 | 
			
		||||
                element.closest('.form-group').append(error);
 | 
			
		||||
            },
 | 
			
		||||
            highlight: function (element, errorClass, validClass) {
 | 
			
		||||
                $(element).addClass('is-invalid');
 | 
			
		||||
            },
 | 
			
		||||
            unhighlight: function (element, errorClass, validClass) {
 | 
			
		||||
                $(element).removeClass('is-invalid');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{{ end }}
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +42,12 @@ Wireguard Server Settings
 | 
			
		|||
                                <input type="text" class="form-control" id="post_up" name="post_up"
 | 
			
		||||
                                       placeholder="Post Up Script" value="{{ .serverInterface.PostUp }}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label for="pre_down">Pre Down Script</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="pre_down" name="pre_down"
 | 
			
		||||
                                       placeholder="Pre Down Script" value="{{ .serverInterface.PreDown }}">
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label for="post_down">Post Down Script</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="post_down" name="post_down"
 | 
			
		||||
| 
						 | 
				
			
			@ -110,7 +116,7 @@ Wireguard Server Settings
 | 
			
		|||
            </div>
 | 
			
		||||
            <div class="modal-body">
 | 
			
		||||
                <p>Are you sure to generate a new key pair for the Wireguard server?<br/>
 | 
			
		||||
                The existing Clients's peer public key need to be updated to keep the connection working.</p>
 | 
			
		||||
                The existing Client's peer public key need to be updated to keep the connection working.</p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-footer justify-content-between">
 | 
			
		||||
                <button type="button" class="btn btn-outline-dark" data-dismiss="modal">Cancel</button>
 | 
			
		||||
| 
						 | 
				
			
			@ -130,13 +136,14 @@ Wireguard Server Settings
 | 
			
		|||
            const addresses = $("#addresses").val().split(",");
 | 
			
		||||
            const listen_port = $("#listen_port").val();
 | 
			
		||||
            const post_up = $("#post_up").val();
 | 
			
		||||
            const pre_down = $("#pre_down").val();
 | 
			
		||||
            const post_down = $("#post_down").val();
 | 
			
		||||
            const data = {"addresses": addresses, "listen_port": listen_port, "post_up": post_up, "post_down": post_down};
 | 
			
		||||
            const data = {"addresses": addresses, "listen_port": listen_port, "post_up": post_up, "pre_down": pre_down, "post_down": post_down};
 | 
			
		||||
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                url: '/wg-server/interfaces',
 | 
			
		||||
                url: '{{.basePath}}/wg-server/interfaces',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
| 
						 | 
				
			
			@ -160,11 +167,13 @@ Wireguard Server Settings
 | 
			
		|||
            'defaultText': 'Add More',
 | 
			
		||||
            'removeWithBackspace': true,
 | 
			
		||||
            'minChars': 0,
 | 
			
		||||
            'minInputWidth': '100%',
 | 
			
		||||
            'placeholderColor': '#666666'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Load server addresses to the form
 | 
			
		||||
        {{range .serverInterface.Addresses}}
 | 
			
		||||
        $("#addresses").removeTag('{{.}}');
 | 
			
		||||
        $("#addresses").addTag('{{.}}');
 | 
			
		||||
        {{end}}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -210,7 +219,7 @@ Wireguard Server Settings
 | 
			
		|||
                $.ajax({
 | 
			
		||||
                    cache: false,
 | 
			
		||||
                    method: 'POST',
 | 
			
		||||
                    url: '/wg-server/keypair',
 | 
			
		||||
                    url: '{{.basePath}}/wg-server/keypair',
 | 
			
		||||
                    dataType: 'json',
 | 
			
		||||
                    contentType: "application/json",
 | 
			
		||||
                    success: function(data) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,20 @@ Connected Peers
 | 
			
		|||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "page_content"}}
 | 
			
		||||
<script>
 | 
			
		||||
  function bytesToHumanReadable(temporal) {
 | 
			
		||||
    const units = [" ", " K", " M", " G", " T", " P", " E", " Z", " Y"]
 | 
			
		||||
    let pow = 0
 | 
			
		||||
 | 
			
		||||
    while (temporal > 1024) {
 | 
			
		||||
      temporal /= 1024
 | 
			
		||||
      pow ++
 | 
			
		||||
      if (pow == units.length-1) break
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return parseFloat(temporal.toFixed(2)) + units[pow]+"B"
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
<section class="content">
 | 
			
		||||
    <div class="container-fluid">
 | 
			
		||||
        {{ if .error }}
 | 
			
		||||
| 
						 | 
				
			
			@ -27,11 +41,13 @@ Connected Peers
 | 
			
		|||
                  <th scope="col">#</th>
 | 
			
		||||
                  <th scope="col">Name</th>
 | 
			
		||||
                  <th scope="col">Email</th>
 | 
			
		||||
                  <th scope="col">Allocated IPs</th>
 | 
			
		||||
                  <th scope="col">Endpoint</th>
 | 
			
		||||
                  <th scope="col">Public Key</th>
 | 
			
		||||
                  <th scope="col">ReceiveBytes</th>
 | 
			
		||||
                  <th scope="col">TransmitBytes</th>
 | 
			
		||||
                  <th scope="col">Received</th>
 | 
			
		||||
                  <th scope="col">Transmitted</th>
 | 
			
		||||
                  <th scope="col">Connected (Approximation)</th>
 | 
			
		||||
                  <th scope="col">LastHandshakeTime</th>
 | 
			
		||||
                  <th scope="col">Last Handshake</th>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </thead>
 | 
			
		||||
              <tbody>
 | 
			
		||||
| 
						 | 
				
			
			@ -40,11 +56,13 @@ Connected Peers
 | 
			
		|||
                <th scope="row">{{ $idx }}</th>
 | 
			
		||||
                <td>{{ $peer.Name }}</td>
 | 
			
		||||
                <td>{{ $peer.Email }}</td>
 | 
			
		||||
                <td>{{ $peer.AllocatedIP }}</td>
 | 
			
		||||
                <td>{{ $peer.Endpoint }}</td>
 | 
			
		||||
                <td>{{ $peer.PublicKey }}</td>
 | 
			
		||||
                <td>{{ $peer.ReceivedBytes }}</td>
 | 
			
		||||
                <td>{{ $peer.TransmitBytes }}</td>
 | 
			
		||||
                <td>{{ $peer.Connected }}</td>
 | 
			
		||||
                <td>{{ $peer.LastHandshakeTime }}</td>
 | 
			
		||||
                <td title="{{ $peer.ReceivedBytes }} Bytes"><script>document.write(bytesToHumanReadable({{ $peer.ReceivedBytes }}))</script></td>
 | 
			
		||||
                <td title="{{ $peer.TransmitBytes }} Bytes"><script>document.write(bytesToHumanReadable({{ $peer.TransmitBytes }}))</script></td>
 | 
			
		||||
                <td>{{ if $peer.Connected }}✓{{end}}</td>
 | 
			
		||||
                <td>{{ $peer.LastHandshakeTime.Format "2006-01-02 15:04:05 MST" }}</td>
 | 
			
		||||
               </tr>
 | 
			
		||||
              {{ end }}
 | 
			
		||||
              </tbody>
 | 
			
		||||
| 
						 | 
				
			
			@ -54,4 +72,4 @@ Connected Peers
 | 
			
		|||
</section>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{define "bottom_js"}}
 | 
			
		||||
{{end}}
 | 
			
		||||
{{end}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,294 @@
 | 
			
		|||
{{define "title"}}
 | 
			
		||||
Users Settings
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "top_css"}}
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "username"}}
 | 
			
		||||
{{ .username }}
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "page_title"}}
 | 
			
		||||
Users Settings
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "page_content"}}
 | 
			
		||||
<section class="content">
 | 
			
		||||
    <div class="container-fluid">
 | 
			
		||||
        <div class="row" id="users-list">
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
<div class="modal fade" id="modal_edit_user">
 | 
			
		||||
    <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h4 class="modal-title">Edit User</h4>
 | 
			
		||||
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | 
			
		||||
                    <span aria-hidden="true">×</span>
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <form name="frm_edit_user" id="frm_edit_user">
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                    <div class="form-group" style="display:none">
 | 
			
		||||
                        <input type="text" style="display:none" class="form-control" id="_previous_user_name"
 | 
			
		||||
                               name="_previous_user_name">
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="_user_name" class="control-label">Name</label>
 | 
			
		||||
                        <input type="text" class="form-control" id="_user_name" name="_user_name">
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="_user_password" class="control-label">Password</label>
 | 
			
		||||
                        <input type="password" class="form-control" id="_user_password" name="_user_password" value=""
 | 
			
		||||
                               placeholder="Leave empty to keep the password unchanged">
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <div class="icheck-primary d-inline">
 | 
			
		||||
                            <input type="checkbox" id="_admin">
 | 
			
		||||
                            <label for="_admin">
 | 
			
		||||
                                Admin
 | 
			
		||||
                            </label>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer justify-content-between">
 | 
			
		||||
                    <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
 | 
			
		||||
                    <button type="submit" class="btn btn-success">Save</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- /.modal-content -->
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- /.modal-dialog -->
 | 
			
		||||
</div>
 | 
			
		||||
<!-- /.modal -->
 | 
			
		||||
 | 
			
		||||
<div class="modal fade" id="modal_remove_user">
 | 
			
		||||
    <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content bg-danger">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h4 class="modal-title">Remove</h4>
 | 
			
		||||
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | 
			
		||||
                    <span aria-hidden="true">×</span>
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-body">
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-footer justify-content-between">
 | 
			
		||||
                <button type="button" class="btn btn-outline-dark" data-dismiss="modal">Cancel</button>
 | 
			
		||||
                <button type="button" class="btn btn-outline-dark" id="remove_user_confirm">Apply</button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- /.modal-content -->
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- /.modal-dialog -->
 | 
			
		||||
</div>
 | 
			
		||||
<!-- /.modal -->
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "bottom_js"}}
 | 
			
		||||
<script>
 | 
			
		||||
    function populateUsersList() {
 | 
			
		||||
        $.ajax({
 | 
			
		||||
            cache: false,
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
            url: '{{.basePath}}/get-users',
 | 
			
		||||
            dataType: 'json',
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            success: function (data) {
 | 
			
		||||
                renderUserList(data);
 | 
			
		||||
            },
 | 
			
		||||
            error: function (jqXHR, exception) {
 | 
			
		||||
                const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                toastr.error(responseJson['message']);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
<script>
 | 
			
		||||
    // load user list
 | 
			
		||||
    $(document).ready(function () {
 | 
			
		||||
        populateUsersList();
 | 
			
		||||
        let newUserHtml = '<div class="col-sm-2 offset-md-4" style=" text-align: right;">' +
 | 
			
		||||
            '<button style="" id="btn_new_user" type="button" class="btn btn-outline-primary btn-sm" ' +
 | 
			
		||||
            'data-toggle="modal" data-target="#modal_edit_user" data-username="">' +
 | 
			
		||||
            '<i class="nav-icon fas fa-plus"></i> New User</button></div>';
 | 
			
		||||
        $('h1').parents(".row").append(newUserHtml);
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // modal_remove_user modal event
 | 
			
		||||
    $("#modal_remove_user").on('show.bs.modal', function (event) {
 | 
			
		||||
        const button = $(event.relatedTarget);
 | 
			
		||||
        const user_name = button.data('username');
 | 
			
		||||
        const modal = $(this);
 | 
			
		||||
        modal.find('.modal-body').text("You are about to remove user " + user_name);
 | 
			
		||||
        modal.find('#remove_user_confirm').val(user_name);
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // remove_user_confirm button event
 | 
			
		||||
    $(document).ready(function () {
 | 
			
		||||
        $("#remove_user_confirm").click(function () {
 | 
			
		||||
            const user_name = $(this).val();
 | 
			
		||||
            const data = {"username": user_name};
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                url: '{{.basePath}}/remove-user',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
                success: function (data) {
 | 
			
		||||
                    $("#modal_remove_user").modal('hide');
 | 
			
		||||
                    toastr.success('Removed user successfully');
 | 
			
		||||
                    const divElement = document.getElementById('user_' + user_name);
 | 
			
		||||
                    divElement.style.display = "none";
 | 
			
		||||
                    location.reload()
 | 
			
		||||
                },
 | 
			
		||||
                error: function (jqXHR, exception) {
 | 
			
		||||
                    const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                    toastr.error(responseJson['message']);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Edit user modal event
 | 
			
		||||
    $(document).ready(function () {
 | 
			
		||||
        $("#modal_edit_user").on('show.bs.modal', function (event) {
 | 
			
		||||
            let modal = $(this);
 | 
			
		||||
            const button = $(event.relatedTarget);
 | 
			
		||||
            const user_name = button.data('username');
 | 
			
		||||
 | 
			
		||||
            // update user modal data
 | 
			
		||||
            if (user_name !== "") {
 | 
			
		||||
                $.ajax({
 | 
			
		||||
                    cache: false,
 | 
			
		||||
                    method: 'GET',
 | 
			
		||||
                    url: '{{.basePath}}/api/user/' + user_name,
 | 
			
		||||
                    dataType: 'json',
 | 
			
		||||
                    contentType: "application/json",
 | 
			
		||||
                    success: function (resp) {
 | 
			
		||||
                        const user = resp;
 | 
			
		||||
 | 
			
		||||
                        modal.find(".modal-title").text("Edit user " + user.username);
 | 
			
		||||
                        modal.find("#_user_name").val(user.username);
 | 
			
		||||
                        modal.find("#_previous_user_name").val(user.username);
 | 
			
		||||
                        modal.find("#_user_password").val("");
 | 
			
		||||
                        modal.find("#_user_password").prop("placeholder", "Leave empty to keep the password unchanged")
 | 
			
		||||
                        modal.find("#_admin").prop("checked", user.admin);
 | 
			
		||||
                    },
 | 
			
		||||
                    error: function (jqXHR, exception) {
 | 
			
		||||
                        const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                        toastr.error(responseJson['message']);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                modal.find(".modal-title").text("Add new user");
 | 
			
		||||
                modal.find("#_user_name").val("");
 | 
			
		||||
                modal.find("#_previous_user_name").val("");
 | 
			
		||||
                modal.find("#_user_password").val("");
 | 
			
		||||
                modal.find("#_user_password").prop("placeholder", "")
 | 
			
		||||
                modal.find("#_admin").prop("checked", false);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function updateUserInfo() {
 | 
			
		||||
        const username = $("#_user_name").val();
 | 
			
		||||
        const previous_username = $("#_previous_user_name").val();
 | 
			
		||||
        const password = $("#_user_password").val();
 | 
			
		||||
        let admin = false;
 | 
			
		||||
        if ($("#_admin").is(':checked')) {
 | 
			
		||||
            admin = true;
 | 
			
		||||
        }
 | 
			
		||||
        const data = {
 | 
			
		||||
            "username": username,
 | 
			
		||||
            "password": password,
 | 
			
		||||
            "previous_username": previous_username,
 | 
			
		||||
            "admin": admin
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (previous_username !== "") {
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                url: '{{.basePath}}/update-user',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
                success: function (data) {
 | 
			
		||||
                    toastr.success("Updated user information successfully");
 | 
			
		||||
                    location.reload();
 | 
			
		||||
                },
 | 
			
		||||
                error: function (jqXHR, exception) {
 | 
			
		||||
                    const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                    toastr.error(responseJson['message']);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                cache: false,
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                url: '{{.basePath}}/create-user',
 | 
			
		||||
                dataType: 'json',
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(data),
 | 
			
		||||
                success: function (data) {
 | 
			
		||||
                    toastr.success("Created user successfully");
 | 
			
		||||
                    location.reload();
 | 
			
		||||
                },
 | 
			
		||||
                error: function (jqXHR, exception) {
 | 
			
		||||
                    const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
			
		||||
                    toastr.error(responseJson['message']);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $(document).ready(function () {
 | 
			
		||||
        $.validator.setDefaults({
 | 
			
		||||
            submitHandler: function (form) {
 | 
			
		||||
                updateUserInfo();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        // Edit user form validation
 | 
			
		||||
        $("#frm_edit_user").validate({
 | 
			
		||||
            rules: {
 | 
			
		||||
                _user_name: {
 | 
			
		||||
                    required: true
 | 
			
		||||
                },
 | 
			
		||||
                _user_password: {
 | 
			
		||||
                    required: function () {
 | 
			
		||||
                        return $("#_previous_user_name").val() === "";
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            messages: {
 | 
			
		||||
                _user_name: {
 | 
			
		||||
                    required: "Please enter a username"
 | 
			
		||||
                },
 | 
			
		||||
                _user_password: {
 | 
			
		||||
                    required: "Please input a password"
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            errorElement: 'span',
 | 
			
		||||
            errorPlacement: function (error, element) {
 | 
			
		||||
                error.addClass('invalid-feedback');
 | 
			
		||||
                element.closest('.form-group').append(error);
 | 
			
		||||
            },
 | 
			
		||||
            highlight: function (element, errorClass, validClass) {
 | 
			
		||||
                $(element).addClass('is-invalid');
 | 
			
		||||
            },
 | 
			
		||||
            unhighlight: function (element, errorClass, validClass) {
 | 
			
		||||
                $(element).removeClass('is-invalid');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        //
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{{end}}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,123 @@
 | 
			
		|||
{{define "title"}}
 | 
			
		||||
    Wake On Lan Hosts
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "top_css"}}
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "username"}}
 | 
			
		||||
    {{ .username }}
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "page_title"}}
 | 
			
		||||
    Wake On Lan Hosts
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "page_content"}}
 | 
			
		||||
 | 
			
		||||
    <div class="modal fade" id="modal_wake_on_lan_host">
 | 
			
		||||
        <!--    MacAddress      string `json:"MacAddress"`-->
 | 
			
		||||
        <!--    Name            string `json:"Name"`-->
 | 
			
		||||
        <!--    LatestIPAddress string `json:"LatestIPAddress"`-->
 | 
			
		||||
        <div class="modal-dialog">
 | 
			
		||||
            <div class="modal-content">
 | 
			
		||||
                <div class="modal-header">
 | 
			
		||||
                    <h4 class="modal-title">New Wake On Lan Host</h4>
 | 
			
		||||
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | 
			
		||||
                        <span aria-hidden="true">×</span>
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <form name="frm_wake_on_lan_host" id="frm_wake_on_lan_host">
 | 
			
		||||
                    <div class="modal-body">
 | 
			
		||||
                        <input type="hidden" id="frm_wake_on_lan_host_old_mac_address" name="old_mac_address">
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="frm_wake_on_lan_host_name" class="control-label">Name</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="frm_wake_on_lan_host_name" name="name">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="frm_wake_on_lan_host_mac_address" class="control-label">Mac Address</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="frm_wake_on_lan_host_mac_address"
 | 
			
		||||
                                   name="mac_address">
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="modal-footer justify-content-between">
 | 
			
		||||
                        <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
 | 
			
		||||
                        <button type="submit" class="btn btn-primary">Submit</button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <!-- /.modal-content -->
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="modal fade" id="modal_remove_wake_on_lan_host">
 | 
			
		||||
        <div class="modal-dialog">
 | 
			
		||||
            <div class="modal-content bg-danger">
 | 
			
		||||
                <div class="modal-header">
 | 
			
		||||
                    <h4 class="modal-title">Remove</h4>
 | 
			
		||||
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | 
			
		||||
                        <span aria-hidden="true">×</span>
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer justify-content-between">
 | 
			
		||||
                    <button type="button" class="btn btn-outline-dark" data-dismiss="modal">Cancel</button>
 | 
			
		||||
                    <button type="button" class="btn btn-outline-dark" id="remove_wake_on_host_confirm">Apply</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <!-- /.modal-content -->
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- /.modal-dialog -->
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <section class="content">
 | 
			
		||||
        <div class="container-fluid">
 | 
			
		||||
            {{ if .error }}
 | 
			
		||||
                <div class="alert alert-warning" role="alert">{{.error}}</div>
 | 
			
		||||
            {{ end}}
 | 
			
		||||
 | 
			
		||||
            <div class="row">
 | 
			
		||||
                {{ range $idx, $host := .hosts }}
 | 
			
		||||
                    {{- /*gotype: github.com/ngoduykhanh/wireguard-ui/model.WakeOnLanHost*/ -}}
 | 
			
		||||
                    <div class="col-sm-4" id="{{ $host.ResolveResourceName }}">
 | 
			
		||||
                        <div class="info-box">
 | 
			
		||||
                            <div class="info-box-content">
 | 
			
		||||
                                <div class="btn-group">
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-success btn-sm"
 | 
			
		||||
                                            data-mac-address="{{ .MacAddress }}">Wake On
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <button type="button"
 | 
			
		||||
                                            class="btn btn-outline-primary btn-sm btn_modify_wake_on_lan_host"
 | 
			
		||||
                                            data-toggle="modal" data-target="#modal_wake_on_lan_host"
 | 
			
		||||
                                            data-name="{{ .Name }}" data-mac-address="{{ .MacAddress }}">Edit
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal"
 | 
			
		||||
                                            data-target="#modal_remove_wake_on_lan_host"
 | 
			
		||||
                                            data-mac-address="{{ .MacAddress }}">Remove
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <hr>
 | 
			
		||||
                                <span class="info-box-text"><i class="fas fa-address-card"></i> <span class="name">{{ .Name }}</span></span>
 | 
			
		||||
                                <span class="info-box-text"><i class="fas fa-ethernet"></i> <span class="mac-address">{{ .MacAddress }}</span></span>
 | 
			
		||||
                                <span class="info-box-text"><i class="fas fa-clock"></i>
 | 
			
		||||
                                    <span class="latest-used">
 | 
			
		||||
                                        {{ if .LatestUsed }}
 | 
			
		||||
                                            {{ .LatestUsed.Format "2006-01-02T15:04:05Z07:00"}}
 | 
			
		||||
                                        {{ else }}
 | 
			
		||||
                                            Unused
 | 
			
		||||
                                        {{ end }}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                {{ end }}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </section>
 | 
			
		||||
 | 
			
		||||
{{end}}
 | 
			
		||||
{{define "bottom_js"}}
 | 
			
		||||
    <script src="{{.basePath}}/static/custom/js/wake_on_lan_hosts.js"></script>
 | 
			
		||||
{{end}}
 | 
			
		||||
| 
						 | 
				
			
			@ -9,16 +9,25 @@ ListenPort = {{ .serverConfig.Interface.ListenPort }}
 | 
			
		|||
PrivateKey = {{ .serverConfig.KeyPair.PrivateKey }}
 | 
			
		||||
{{if .globalSettings.MTU}}MTU = {{ .globalSettings.MTU }}{{end}}
 | 
			
		||||
PostUp = {{ .serverConfig.Interface.PostUp }}
 | 
			
		||||
PreDown = {{ .serverConfig.Interface.PreDown }}
 | 
			
		||||
PostDown = {{ .serverConfig.Interface.PostDown }}
 | 
			
		||||
Table = {{ .globalSettings.Table }}
 | 
			
		||||
 | 
			
		||||
{{range .clientDataList}}{{if eq .Client.Enabled true}}
 | 
			
		||||
# ID:           {{ .Client.ID }}
 | 
			
		||||
# Name:         {{ .Client.Name }}
 | 
			
		||||
# Email:        {{ .Client.Email }}
 | 
			
		||||
# Telegram:     {{ .Client.TgUserid }}
 | 
			
		||||
# Created at:   {{ .Client.CreatedAt }}
 | 
			
		||||
# Update at:    {{ .Client.UpdatedAt }}
 | 
			
		||||
{{- if .Client.AdditionalNotes}}
 | 
			
		||||
 | 
			
		||||
# Notes:
 | 
			
		||||
# {{ .Client.AdditionalNotes }}{{end}}
 | 
			
		||||
[Peer]
 | 
			
		||||
PublicKey = {{ .Client.PublicKey }}
 | 
			
		||||
PresharedKey = {{ .Client.PresharedKey }}
 | 
			
		||||
AllowedIPs = {{$first :=true}}{{range .Client.AllocatedIPs }}{{if $first}}{{$first = false}}{{else}},{{end}}{{.}}{{end}}
 | 
			
		||||
{{if .Client.PresharedKey}}PresharedKey = {{ .Client.PresharedKey }}{{end}}
 | 
			
		||||
AllowedIPs = {{$first :=true}}{{range .Client.AllocatedIPs }}{{if $first}}{{$first = false}}{{else}},{{end}}{{.}}{{end}}{{range .Client.ExtraAllowedIPs }},{{.}}{{end}}
 | 
			
		||||
{{if $.globalSettings.PersistentKeepalive}}PersistentKeepalive = {{ $.globalSettings.PersistentKeepalive }}{{end}}
 | 
			
		||||
{{if .Client.Endpoint}}Endpoint = {{ .Client.Endpoint }}{{end}}
 | 
			
		||||
{{end}}{{end}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
package util
 | 
			
		||||
 | 
			
		||||
import "sync"
 | 
			
		||||
 | 
			
		||||
var IPToSubnetRange = map[string]uint16{}
 | 
			
		||||
var TgUseridToClientID = map[int64][]string{}
 | 
			
		||||
var TgUseridToClientIDMutex sync.RWMutex
 | 
			
		||||
var DBUsersToCRC32 = map[string]uint32{}
 | 
			
		||||
							
								
								
									
										129
									
								
								util/config.go
								
								
								
								
							
							
						
						
									
										129
									
								
								util/config.go
								
								
								
								
							| 
						 | 
				
			
			@ -1,26 +1,119 @@
 | 
			
		|||
package util
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/labstack/gommon/log"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Runtime config
 | 
			
		||||
var (
 | 
			
		||||
	DisableLogin   bool
 | 
			
		||||
	BindAddress    string
 | 
			
		||||
	SendgridApiKey string
 | 
			
		||||
	EmailFrom      string
 | 
			
		||||
	EmailFromName  string
 | 
			
		||||
	EmailSubject   string
 | 
			
		||||
	EmailContent   string
 | 
			
		||||
	SessionSecret  []byte
 | 
			
		||||
	DisableLogin       bool
 | 
			
		||||
	BindAddress        string
 | 
			
		||||
	SmtpHostname       string
 | 
			
		||||
	SmtpPort           int
 | 
			
		||||
	SmtpUsername       string
 | 
			
		||||
	SmtpPassword       string
 | 
			
		||||
	SmtpNoTLSCheck     bool
 | 
			
		||||
	SmtpEncryption     string
 | 
			
		||||
	SmtpAuthType       string
 | 
			
		||||
	SmtpHelo           string
 | 
			
		||||
	SendgridApiKey     string
 | 
			
		||||
	EmailFrom          string
 | 
			
		||||
	EmailFromName      string
 | 
			
		||||
	SessionSecret      [64]byte
 | 
			
		||||
	SessionMaxDuration int64
 | 
			
		||||
	WgConfTemplate     string
 | 
			
		||||
	BasePath           string
 | 
			
		||||
	SubnetRanges       map[string]([]*net.IPNet)
 | 
			
		||||
	SubnetRangesOrder  []string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	DefaultUsername            = "admin"
 | 
			
		||||
	DefaultPassword            = "admin"
 | 
			
		||||
	DefaultServerAddress       = "10.252.1.0/24"
 | 
			
		||||
	DefaultServerPort          = 51820
 | 
			
		||||
	DefaultDNS                 = "1.1.1.1"
 | 
			
		||||
	DefaultMTU                 = 1450
 | 
			
		||||
	DefaultPersistentKeepalive = 15
 | 
			
		||||
	DefaultConfigFilePath      = "/etc/wireguard/wg0.conf"
 | 
			
		||||
	UsernameEnvVar             = "WGUI_USERNAME"
 | 
			
		||||
	PasswordEnvVar             = "WGUI_PASSWORD"
 | 
			
		||||
	DefaultUsername                        = "admin"
 | 
			
		||||
	DefaultPassword                        = "admin"
 | 
			
		||||
	DefaultIsAdmin                         = true
 | 
			
		||||
	DefaultServerAddress                   = "10.252.1.0/24"
 | 
			
		||||
	DefaultServerPort                      = 51820
 | 
			
		||||
	DefaultDNS                             = "1.1.1.1"
 | 
			
		||||
	DefaultMTU                             = 1450
 | 
			
		||||
	DefaultPersistentKeepalive             = 15
 | 
			
		||||
	DefaultFirewallMark                    = "0xca6c" // i.e. 51820
 | 
			
		||||
	DefaultTable                           = "auto"
 | 
			
		||||
	DefaultConfigFilePath                  = "/etc/wireguard/wg0.conf"
 | 
			
		||||
	UsernameEnvVar                         = "WGUI_USERNAME"
 | 
			
		||||
	PasswordEnvVar                         = "WGUI_PASSWORD"
 | 
			
		||||
	PasswordFileEnvVar                     = "WGUI_PASSWORD_FILE"
 | 
			
		||||
	PasswordHashEnvVar                     = "WGUI_PASSWORD_HASH"
 | 
			
		||||
	PasswordHashFileEnvVar                 = "WGUI_PASSWORD_HASH_FILE"
 | 
			
		||||
	FaviconFilePathEnvVar                  = "WGUI_FAVICON_FILE_PATH"
 | 
			
		||||
	EndpointAddressEnvVar                  = "WGUI_ENDPOINT_ADDRESS"
 | 
			
		||||
	DNSEnvVar                              = "WGUI_DNS"
 | 
			
		||||
	MTUEnvVar                              = "WGUI_MTU"
 | 
			
		||||
	PersistentKeepaliveEnvVar              = "WGUI_PERSISTENT_KEEPALIVE"
 | 
			
		||||
	FirewallMarkEnvVar                     = "WGUI_FIREWALL_MARK"
 | 
			
		||||
	TableEnvVar                            = "WGUI_TABLE"
 | 
			
		||||
	ConfigFilePathEnvVar                   = "WGUI_CONFIG_FILE_PATH"
 | 
			
		||||
	LogLevel                               = "WGUI_LOG_LEVEL"
 | 
			
		||||
	ServerAddressesEnvVar                  = "WGUI_SERVER_INTERFACE_ADDRESSES"
 | 
			
		||||
	ServerListenPortEnvVar                 = "WGUI_SERVER_LISTEN_PORT"
 | 
			
		||||
	ServerPostUpScriptEnvVar               = "WGUI_SERVER_POST_UP_SCRIPT"
 | 
			
		||||
	ServerPostDownScriptEnvVar             = "WGUI_SERVER_POST_DOWN_SCRIPT"
 | 
			
		||||
	DefaultClientAllowedIpsEnvVar          = "WGUI_DEFAULT_CLIENT_ALLOWED_IPS"
 | 
			
		||||
	DefaultClientExtraAllowedIpsEnvVar     = "WGUI_DEFAULT_CLIENT_EXTRA_ALLOWED_IPS"
 | 
			
		||||
	DefaultClientUseServerDNSEnvVar        = "WGUI_DEFAULT_CLIENT_USE_SERVER_DNS"
 | 
			
		||||
	DefaultClientEnableAfterCreationEnvVar = "WGUI_DEFAULT_CLIENT_ENABLE_AFTER_CREATION"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func ParseBasePath(basePath string) string {
 | 
			
		||||
	if !strings.HasPrefix(basePath, "/") {
 | 
			
		||||
		basePath = "/" + basePath
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasSuffix(basePath, "/") {
 | 
			
		||||
		basePath = strings.TrimSuffix(basePath, "/")
 | 
			
		||||
	}
 | 
			
		||||
	return basePath
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseSubnetRanges(subnetRangesStr string) map[string]([]*net.IPNet) {
 | 
			
		||||
	subnetRanges := map[string]([]*net.IPNet){}
 | 
			
		||||
	if subnetRangesStr == "" {
 | 
			
		||||
		return subnetRanges
 | 
			
		||||
	}
 | 
			
		||||
	cidrSet := map[string]bool{}
 | 
			
		||||
	subnetRangesStr = strings.TrimSpace(subnetRangesStr)
 | 
			
		||||
	subnetRangesStr = strings.Trim(subnetRangesStr, ";:,")
 | 
			
		||||
	ranges := strings.Split(subnetRangesStr, ";")
 | 
			
		||||
	for _, rng := range ranges {
 | 
			
		||||
		rng = strings.TrimSpace(rng)
 | 
			
		||||
		rngSpl := strings.Split(rng, ":")
 | 
			
		||||
		if len(rngSpl) != 2 {
 | 
			
		||||
			log.Warnf("Unable to parse subnet range: %v. Skipped.", rng)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		rngName := strings.TrimSpace(rngSpl[0])
 | 
			
		||||
		subnetRanges[rngName] = make([]*net.IPNet, 0)
 | 
			
		||||
		cidrs := strings.Split(rngSpl[1], ",")
 | 
			
		||||
		for _, cidr := range cidrs {
 | 
			
		||||
			cidr = strings.TrimSpace(cidr)
 | 
			
		||||
			_, net, err := net.ParseCIDR(cidr)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Warnf("[%v] Unable to parse CIDR: %v. Skipped.", rngName, cidr)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if cidrSet[net.String()] {
 | 
			
		||||
				log.Warnf("[%v] CIDR already exists: %v. Skipped.", rngName, net.String())
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			cidrSet[net.String()] = true
 | 
			
		||||
			subnetRanges[rngName] = append(subnetRanges[rngName], net)
 | 
			
		||||
		}
 | 
			
		||||
		if len(subnetRanges[rngName]) == 0 {
 | 
			
		||||
			delete(subnetRanges, rngName)
 | 
			
		||||
		} else {
 | 
			
		||||
			SubnetRangesOrder = append(SubnetRangesOrder, rngName)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return subnetRanges
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
package util
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func HashPassword(plaintext string) (string, error) {
 | 
			
		||||
	bytes, err := bcrypt.GenerateFromPassword([]byte(plaintext), 14)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("cannot hash password: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	return base64.StdEncoding.EncodeToString(bytes), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func VerifyHash(base64Hash string, plaintext string) (bool, error) {
 | 
			
		||||
	hash, err := base64.StdEncoding.DecodeString(base64Hash)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, fmt.Errorf("cannot decode base64 hash: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	err = bcrypt.CompareHashAndPassword(hash, []byte(plaintext))
 | 
			
		||||
	if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
 | 
			
		||||
		return false, nil
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, fmt.Errorf("cannot verify password: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										560
									
								
								util/util.go
								
								
								
								
							
							
						
						
									
										560
									
								
								util/util.go
								
								
								
								
							| 
						 | 
				
			
			@ -1,37 +1,64 @@
 | 
			
		|||
package util
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"hash/crc32"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"net"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"text/template"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	rice "github.com/GeertJohan/go.rice"
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/store"
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/telegram"
 | 
			
		||||
	"github.com/skip2/go-qrcode"
 | 
			
		||||
	"golang.org/x/mod/sumdb/dirhash"
 | 
			
		||||
 | 
			
		||||
	externalip "github.com/glendc/go-external-ip"
 | 
			
		||||
	"github.com/labstack/gommon/log"
 | 
			
		||||
	"github.com/ngoduykhanh/wireguard-ui/model"
 | 
			
		||||
	"github.com/sdomino/scribble"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var qrCodeSettings = model.QRCodeSettings{
 | 
			
		||||
	Enabled:    true,
 | 
			
		||||
	IncludeDNS: true,
 | 
			
		||||
	IncludeMTU: true,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BuildClientConfig to create wireguard client config string
 | 
			
		||||
func BuildClientConfig(client model.Client, server model.Server, setting model.GlobalSetting) string {
 | 
			
		||||
	// Interface section
 | 
			
		||||
	clientAddress := fmt.Sprintf("Address = %s", strings.Join(client.AllocatedIPs, ","))
 | 
			
		||||
	clientPrivateKey := fmt.Sprintf("PrivateKey = %s", client.PrivateKey)
 | 
			
		||||
	clientAddress := fmt.Sprintf("Address = %s\n", strings.Join(client.AllocatedIPs, ","))
 | 
			
		||||
	clientPrivateKey := fmt.Sprintf("PrivateKey = %s\n", client.PrivateKey)
 | 
			
		||||
	clientDNS := ""
 | 
			
		||||
	if client.UseServerDNS {
 | 
			
		||||
		clientDNS = fmt.Sprintf("DNS = %s", strings.Join(setting.DNSServers, ","))
 | 
			
		||||
		clientDNS = fmt.Sprintf("DNS = %s\n", strings.Join(setting.DNSServers, ","))
 | 
			
		||||
	}
 | 
			
		||||
	clientMTU := ""
 | 
			
		||||
	if setting.MTU > 0 {
 | 
			
		||||
		clientMTU = fmt.Sprintf("MTU = %d\n", setting.MTU)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Peer section
 | 
			
		||||
	peerPublicKey := fmt.Sprintf("PublicKey = %s", server.KeyPair.PublicKey)
 | 
			
		||||
	peerPresharedKey := fmt.Sprintf("PresharedKey = %s", client.PresharedKey)
 | 
			
		||||
	peerAllowedIPs := fmt.Sprintf("AllowedIPs = %s", strings.Join(client.AllowedIPs, ","))
 | 
			
		||||
	peerPublicKey := fmt.Sprintf("PublicKey = %s\n", server.KeyPair.PublicKey)
 | 
			
		||||
	peerPresharedKey := ""
 | 
			
		||||
	if client.PresharedKey != "" {
 | 
			
		||||
		peerPresharedKey = fmt.Sprintf("PresharedKey = %s\n", client.PresharedKey)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	peerAllowedIPs := fmt.Sprintf("AllowedIPs = %s\n", strings.Join(client.AllowedIPs, ","))
 | 
			
		||||
 | 
			
		||||
	desiredHost := setting.EndpointAddress
 | 
			
		||||
	desiredPort := server.Interface.ListenPort
 | 
			
		||||
| 
						 | 
				
			
			@ -44,28 +71,49 @@ func BuildClientConfig(client model.Client, server model.Server, setting model.G
 | 
			
		|||
			log.Error("Endpoint appears to be incorrectly formatted: ", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	peerEndpoint := fmt.Sprintf("Endpoint = %s:%d", desiredHost, desiredPort)
 | 
			
		||||
	peerEndpoint := fmt.Sprintf("Endpoint = %s:%d\n", desiredHost, desiredPort)
 | 
			
		||||
 | 
			
		||||
	peerPersistentKeepalive := ""
 | 
			
		||||
	if setting.PersistentKeepalive > 0 {
 | 
			
		||||
		peerPersistentKeepalive = fmt.Sprintf("PersistentKeepalive = %d", setting.PersistentKeepalive)
 | 
			
		||||
		peerPersistentKeepalive = fmt.Sprintf("PersistentKeepalive = %d\n", setting.PersistentKeepalive)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// build the config as string
 | 
			
		||||
	strConfig := "[Interface]\n" +
 | 
			
		||||
		clientAddress + "\n" +
 | 
			
		||||
		clientPrivateKey + "\n" +
 | 
			
		||||
		clientDNS + "\n\n" +
 | 
			
		||||
		"[Peer]" + "\n" +
 | 
			
		||||
		peerPublicKey + "\n" +
 | 
			
		||||
		peerPresharedKey + "\n" +
 | 
			
		||||
		peerAllowedIPs + "\n" +
 | 
			
		||||
		peerEndpoint + "\n" +
 | 
			
		||||
		peerPersistentKeepalive + "\n"
 | 
			
		||||
		clientAddress +
 | 
			
		||||
		clientPrivateKey +
 | 
			
		||||
		clientDNS +
 | 
			
		||||
		clientMTU +
 | 
			
		||||
		"\n[Peer]\n" +
 | 
			
		||||
		peerPublicKey +
 | 
			
		||||
		peerPresharedKey +
 | 
			
		||||
		peerAllowedIPs +
 | 
			
		||||
		peerEndpoint +
 | 
			
		||||
		peerPersistentKeepalive
 | 
			
		||||
 | 
			
		||||
	return strConfig
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClientDefaultsFromEnv to read the default values for creating a new client from the environment or use sane defaults
 | 
			
		||||
func ClientDefaultsFromEnv() model.ClientDefaults {
 | 
			
		||||
	clientDefaults := model.ClientDefaults{}
 | 
			
		||||
	clientDefaults.AllowedIps = LookupEnvOrStrings(DefaultClientAllowedIpsEnvVar, []string{"0.0.0.0/0"})
 | 
			
		||||
	clientDefaults.ExtraAllowedIps = LookupEnvOrStrings(DefaultClientExtraAllowedIpsEnvVar, []string{})
 | 
			
		||||
	clientDefaults.UseServerDNS = LookupEnvOrBool(DefaultClientUseServerDNSEnvVar, true)
 | 
			
		||||
	clientDefaults.EnableAfterCreation = LookupEnvOrBool(DefaultClientEnableAfterCreationEnvVar, true)
 | 
			
		||||
 | 
			
		||||
	return clientDefaults
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ContainsCIDR to check if ipnet1 contains ipnet2
 | 
			
		||||
// https://stackoverflow.com/a/40406619/6111641
 | 
			
		||||
// https://go.dev/play/p/Q4J-JEN3sF
 | 
			
		||||
func ContainsCIDR(ipnet1, ipnet2 *net.IPNet) bool {
 | 
			
		||||
	ones1, _ := ipnet1.Mask.Size()
 | 
			
		||||
	ones2, _ := ipnet2.Mask.Size()
 | 
			
		||||
	return ones1 <= ones2 && ipnet1.Contains(ipnet2.IP)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ValidateCIDR to validate a network CIDR
 | 
			
		||||
func ValidateCIDR(cidr string) bool {
 | 
			
		||||
	_, _, err := net.ParseCIDR(cidr)
 | 
			
		||||
| 
						 | 
				
			
			@ -76,10 +124,18 @@ func ValidateCIDR(cidr string) bool {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// ValidateCIDRList to validate a list of network CIDR
 | 
			
		||||
func ValidateCIDRList(cidrs []string) bool {
 | 
			
		||||
func ValidateCIDRList(cidrs []string, allowEmpty bool) bool {
 | 
			
		||||
	for _, cidr := range cidrs {
 | 
			
		||||
		if ValidateCIDR(cidr) == false {
 | 
			
		||||
			return false
 | 
			
		||||
		if allowEmpty {
 | 
			
		||||
			if len(cidr) > 0 {
 | 
			
		||||
				if ValidateCIDR(cidr) == false {
 | 
			
		||||
					return false
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			if ValidateCIDR(cidr) == false {
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
| 
						 | 
				
			
			@ -87,7 +143,15 @@ func ValidateCIDRList(cidrs []string) bool {
 | 
			
		|||
 | 
			
		||||
// ValidateAllowedIPs to validate allowed ip addresses in CIDR format
 | 
			
		||||
func ValidateAllowedIPs(cidrs []string) bool {
 | 
			
		||||
	if ValidateCIDRList(cidrs) == false {
 | 
			
		||||
	if ValidateCIDRList(cidrs, false) == false {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ValidateExtraAllowedIPs to validate extra Allowed ip addresses, allowing empty strings
 | 
			
		||||
func ValidateExtraAllowedIPs(cidrs []string) bool {
 | 
			
		||||
	if ValidateCIDRList(cidrs, true) == false {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +159,7 @@ func ValidateAllowedIPs(cidrs []string) bool {
 | 
			
		|||
 | 
			
		||||
// ValidateServerAddresses to validate allowed ip addresses in CIDR format
 | 
			
		||||
func ValidateServerAddresses(cidrs []string) bool {
 | 
			
		||||
	if ValidateCIDRList(cidrs) == false {
 | 
			
		||||
	if ValidateCIDRList(cidrs, false) == false {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
| 
						 | 
				
			
			@ -127,7 +191,7 @@ func GetInterfaceIPs() ([]model.Interface, error) {
 | 
			
		|||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var interfaceList = []model.Interface{}
 | 
			
		||||
	var interfaceList []model.Interface
 | 
			
		||||
 | 
			
		||||
	// get interface's ip addresses
 | 
			
		||||
	for _, i := range ifaces {
 | 
			
		||||
| 
						 | 
				
			
			@ -168,9 +232,9 @@ func GetPublicIP() (model.Interface, error) {
 | 
			
		|||
	consensus := externalip.NewConsensus(&cfg, nil)
 | 
			
		||||
 | 
			
		||||
	// add trusted voters
 | 
			
		||||
	consensus.AddVoter(externalip.NewHTTPSource("http://checkip.amazonaws.com/"), 1)
 | 
			
		||||
	consensus.AddVoter(externalip.NewHTTPSource("https://checkip.amazonaws.com/"), 1)
 | 
			
		||||
	consensus.AddVoter(externalip.NewHTTPSource("http://whatismyip.akamai.com"), 1)
 | 
			
		||||
	consensus.AddVoter(externalip.NewHTTPSource("http://ifconfig.top"), 1)
 | 
			
		||||
	consensus.AddVoter(externalip.NewHTTPSource("https://ifconfig.top"), 1)
 | 
			
		||||
 | 
			
		||||
	publicInterface := model.Interface{}
 | 
			
		||||
	publicInterface.Name = "Public Address"
 | 
			
		||||
| 
						 | 
				
			
			@ -178,10 +242,12 @@ func GetPublicIP() (model.Interface, error) {
 | 
			
		|||
	ip, err := consensus.ExternalIP()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		publicInterface.IPAddress = "N/A"
 | 
			
		||||
	} else {
 | 
			
		||||
		publicInterface.IPAddress = ip.String()
 | 
			
		||||
	}
 | 
			
		||||
	publicInterface.IPAddress = ip.String()
 | 
			
		||||
 | 
			
		||||
	return publicInterface, err
 | 
			
		||||
	// error handling happened above, no need to pass it through
 | 
			
		||||
	return publicInterface, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetIPFromCIDR get ip from CIDR
 | 
			
		||||
| 
						 | 
				
			
			@ -228,7 +294,7 @@ func GetAllocatedIPs(ignoreClientID string) ([]string, error) {
 | 
			
		|||
	// append client's addresses to the result
 | 
			
		||||
	for _, f := range records {
 | 
			
		||||
		client := model.Client{}
 | 
			
		||||
		if err := json.Unmarshal([]byte(f), &client); err != nil {
 | 
			
		||||
		if err := json.Unmarshal(f, &client); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -270,17 +336,34 @@ func GetBroadcastIP(n *net.IPNet) net.IP {
 | 
			
		|||
	return broadcast
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetBroadcastAndNetworkAddrsLookup get the ip address that can't be used with current server interfaces
 | 
			
		||||
func GetBroadcastAndNetworkAddrsLookup(interfaceAddresses []string) map[string]bool {
 | 
			
		||||
	list := make(map[string]bool)
 | 
			
		||||
	for _, ifa := range interfaceAddresses {
 | 
			
		||||
		_, netAddr, err := net.ParseCIDR(ifa)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		broadcastAddr := GetBroadcastIP(netAddr).String()
 | 
			
		||||
		networkAddr := netAddr.IP.String()
 | 
			
		||||
		list[broadcastAddr] = true
 | 
			
		||||
		list[networkAddr] = true
 | 
			
		||||
	}
 | 
			
		||||
	return list
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAvailableIP get the ip address that can be allocated from an CIDR
 | 
			
		||||
func GetAvailableIP(cidr string, allocatedList []string) (string, error) {
 | 
			
		||||
	ip, net, err := net.ParseCIDR(cidr)
 | 
			
		||||
// We need interfaceAddresses to find real broadcast and network addresses
 | 
			
		||||
func GetAvailableIP(cidr string, allocatedList, interfaceAddresses []string) (string, error) {
 | 
			
		||||
	ip, netAddr, err := net.ParseCIDR(cidr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	broadcastAddr := GetBroadcastIP(net).String()
 | 
			
		||||
	networkAddr := net.IP.String()
 | 
			
		||||
	unavailableIPs := GetBroadcastAndNetworkAddrsLookup(interfaceAddresses)
 | 
			
		||||
 | 
			
		||||
	for ip := ip.Mask(net.Mask); net.Contains(ip); inc(ip) {
 | 
			
		||||
	for ip := ip.Mask(netAddr.Mask); netAddr.Contains(ip); inc(ip) {
 | 
			
		||||
		available := true
 | 
			
		||||
		suggestedAddr := ip.String()
 | 
			
		||||
		for _, allocatedAddr := range allocatedList {
 | 
			
		||||
| 
						 | 
				
			
			@ -289,7 +372,7 @@ func GetAvailableIP(cidr string, allocatedList []string) (string, error) {
 | 
			
		|||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if available && suggestedAddr != networkAddr && suggestedAddr != broadcastAddr {
 | 
			
		||||
		if available && !unavailableIPs[suggestedAddr] {
 | 
			
		||||
			return suggestedAddr, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -305,7 +388,7 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip
 | 
			
		|||
 | 
			
		||||
		// clientCIDR must be in CIDR format
 | 
			
		||||
		if ip == nil {
 | 
			
		||||
			return false, fmt.Errorf("Invalid ip allocation input %s. Must be in CIDR format", clientCIDR)
 | 
			
		||||
			return false, fmt.Errorf("invalid ip allocation input %s. Must be in CIDR format", clientCIDR)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// return false immediately if the ip is already in use (in ipAllocatedList)
 | 
			
		||||
| 
						 | 
				
			
			@ -317,7 +400,7 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip
 | 
			
		|||
 | 
			
		||||
		// even if it is not in use, we still need to check if it
 | 
			
		||||
		// belongs to a network of the server.
 | 
			
		||||
		var isValid bool = false
 | 
			
		||||
		var isValid = false
 | 
			
		||||
		for _, serverCIDR := range serverAddresses {
 | 
			
		||||
			_, serverNet, _ := net.ParseCIDR(serverCIDR)
 | 
			
		||||
			if serverNet.Contains(ip) {
 | 
			
		||||
| 
						 | 
				
			
			@ -337,13 +420,154 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip
 | 
			
		|||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
 | 
			
		||||
func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, clientDataList []model.ClientData, globalSettings model.GlobalSetting) error {
 | 
			
		||||
	// read wg.conf template file to string
 | 
			
		||||
	tmplWireguardConf, err := tmplBox.String("wg.conf")
 | 
			
		||||
// findSubnetRangeForIP to find first SR for IP, and cache the match
 | 
			
		||||
func findSubnetRangeForIP(cidr string) (uint16, error) {
 | 
			
		||||
	ip, _, err := net.ParseCIDR(cidr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if srName, ok := IPToSubnetRange[ip.String()]; ok {
 | 
			
		||||
		return srName, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for srIndex, sr := range SubnetRangesOrder {
 | 
			
		||||
		for _, srCIDR := range SubnetRanges[sr] {
 | 
			
		||||
			if srCIDR.Contains(ip) {
 | 
			
		||||
				IPToSubnetRange[ip.String()] = uint16(srIndex)
 | 
			
		||||
				return uint16(srIndex), nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return 0, fmt.Errorf("subnet range not found for this IP")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FillClientSubnetRange to fill subnet ranges client belongs to, does nothing if SRs are not found
 | 
			
		||||
func FillClientSubnetRange(client model.ClientData) model.ClientData {
 | 
			
		||||
	cl := *client.Client
 | 
			
		||||
	for _, ip := range cl.AllocatedIPs {
 | 
			
		||||
		sr, err := findSubnetRangeForIP(ip)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		cl.SubnetRanges = append(cl.SubnetRanges, SubnetRangesOrder[sr])
 | 
			
		||||
	}
 | 
			
		||||
	return model.ClientData{
 | 
			
		||||
		Client: &cl,
 | 
			
		||||
		QRCode: client.QRCode,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ValidateAndFixSubnetRanges to check if subnet ranges are valid for the server configuration
 | 
			
		||||
// Removes all non-valid CIDRs
 | 
			
		||||
func ValidateAndFixSubnetRanges(db store.IStore) error {
 | 
			
		||||
	if len(SubnetRangesOrder) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	server, err := db.GetServer()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	var serverSubnets []*net.IPNet
 | 
			
		||||
	for _, addr := range server.Interface.Addresses {
 | 
			
		||||
		addr = strings.TrimSpace(addr)
 | 
			
		||||
		_, netAddr, err := net.ParseCIDR(addr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		serverSubnets = append(serverSubnets, netAddr)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, rng := range SubnetRangesOrder {
 | 
			
		||||
		cidrs := SubnetRanges[rng]
 | 
			
		||||
		if len(cidrs) > 0 {
 | 
			
		||||
			newCIDRs := make([]*net.IPNet, 0)
 | 
			
		||||
			for _, cidr := range cidrs {
 | 
			
		||||
				valid := false
 | 
			
		||||
 | 
			
		||||
				for _, serverSubnet := range serverSubnets {
 | 
			
		||||
					if ContainsCIDR(serverSubnet, cidr) {
 | 
			
		||||
						valid = true
 | 
			
		||||
						break
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if valid {
 | 
			
		||||
					newCIDRs = append(newCIDRs, cidr)
 | 
			
		||||
				} else {
 | 
			
		||||
					log.Warnf("[%v] CIDR is outside of all server subnets: %v. Removed.", rng, cidr)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if len(newCIDRs) > 0 {
 | 
			
		||||
				SubnetRanges[rng] = newCIDRs
 | 
			
		||||
			} else {
 | 
			
		||||
				delete(SubnetRanges, rng)
 | 
			
		||||
				log.Warnf("[%v] No valid CIDRs in this subnet range. Removed.", rng)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSubnetRangesString to get a formatted string, representing active subnet ranges
 | 
			
		||||
func GetSubnetRangesString() string {
 | 
			
		||||
	if len(SubnetRangesOrder) == 0 {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	strB := strings.Builder{}
 | 
			
		||||
 | 
			
		||||
	for _, rng := range SubnetRangesOrder {
 | 
			
		||||
		cidrs := SubnetRanges[rng]
 | 
			
		||||
		if len(cidrs) > 0 {
 | 
			
		||||
			strB.WriteString(rng)
 | 
			
		||||
			strB.WriteString(":[")
 | 
			
		||||
			first := true
 | 
			
		||||
			for _, cidr := range cidrs {
 | 
			
		||||
				if !first {
 | 
			
		||||
					strB.WriteString(", ")
 | 
			
		||||
				}
 | 
			
		||||
				strB.WriteString(cidr.String())
 | 
			
		||||
				first = false
 | 
			
		||||
			}
 | 
			
		||||
			strB.WriteString("]  ")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return strings.TrimSpace(strB.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
 | 
			
		||||
func WriteWireGuardServerConfig(tmplDir fs.FS, serverConfig model.Server, clientDataList []model.ClientData, usersList []model.User, globalSettings model.GlobalSetting) error {
 | 
			
		||||
	var tmplWireguardConf string
 | 
			
		||||
 | 
			
		||||
	// if set, read wg.conf template from WgConfTemplate
 | 
			
		||||
	if len(WgConfTemplate) > 0 {
 | 
			
		||||
		fileContentBytes, err := os.ReadFile(WgConfTemplate)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		tmplWireguardConf = string(fileContentBytes)
 | 
			
		||||
	} else {
 | 
			
		||||
		// read default wg.conf template file to string
 | 
			
		||||
		fileContent, err := StringFromEmbedFile(tmplDir, "wg.conf")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		tmplWireguardConf = fileContent
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// escape multiline notes
 | 
			
		||||
	escapedClientDataList := []model.ClientData{}
 | 
			
		||||
	for _, cd := range clientDataList {
 | 
			
		||||
		if cd.Client.AdditionalNotes != "" {
 | 
			
		||||
			cd.Client.AdditionalNotes = strings.ReplaceAll(cd.Client.AdditionalNotes, "\n", "\n# ")
 | 
			
		||||
		}
 | 
			
		||||
		escapedClientDataList = append(escapedClientDataList, cd)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// parse the template
 | 
			
		||||
	t, err := template.New("wg_config").Parse(tmplWireguardConf)
 | 
			
		||||
| 
						 | 
				
			
			@ -359,8 +583,9 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl
 | 
			
		|||
 | 
			
		||||
	config := map[string]interface{}{
 | 
			
		||||
		"serverConfig":   serverConfig,
 | 
			
		||||
		"clientDataList": clientDataList,
 | 
			
		||||
		"clientDataList": escapedClientDataList,
 | 
			
		||||
		"globalSettings": globalSettings,
 | 
			
		||||
		"usersList":      usersList,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = t.Execute(f, config)
 | 
			
		||||
| 
						 | 
				
			
			@ -372,6 +597,57 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl
 | 
			
		|||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendRequestedConfigsToTelegram to send client all their configs. Returns failed configs list.
 | 
			
		||||
func SendRequestedConfigsToTelegram(db store.IStore, userid int64) []string {
 | 
			
		||||
	failedList := make([]string, 0)
 | 
			
		||||
	TgUseridToClientIDMutex.RLock()
 | 
			
		||||
	if clids, found := TgUseridToClientID[userid]; found && len(clids) > 0 {
 | 
			
		||||
		TgUseridToClientIDMutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
		for _, clid := range clids {
 | 
			
		||||
			clientData, err := db.GetClientByID(clid, qrCodeSettings)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				// return fmt.Errorf("unable to get client")
 | 
			
		||||
				failedList = append(failedList, clid)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// build config
 | 
			
		||||
			server, _ := db.GetServer()
 | 
			
		||||
			globalSettings, _ := db.GetGlobalSettings()
 | 
			
		||||
			config := BuildClientConfig(*clientData.Client, server, globalSettings)
 | 
			
		||||
			configData := []byte(config)
 | 
			
		||||
			var qrData []byte
 | 
			
		||||
 | 
			
		||||
			if clientData.Client.PrivateKey != "" {
 | 
			
		||||
				qrData, err = qrcode.Encode(config, qrcode.Medium, 512)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					// return fmt.Errorf("unable to encode qr")
 | 
			
		||||
					failedList = append(failedList, clientData.Client.Name)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			userid, err := strconv.ParseInt(clientData.Client.TgUserid, 10, 64)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				// return fmt.Errorf("tg usrid is unreadable")
 | 
			
		||||
				failedList = append(failedList, clientData.Client.Name)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = telegram.SendConfig(userid, clientData.Client.Name, configData, qrData, true)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				failedList = append(failedList, clientData.Client.Name)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			time.Sleep(2 * time.Second)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		TgUseridToClientIDMutex.RUnlock()
 | 
			
		||||
	}
 | 
			
		||||
	return failedList
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LookupEnvOrString(key string, defaultVal string) string {
 | 
			
		||||
	if val, ok := os.LookupEnv(key); ok {
 | 
			
		||||
		return val
 | 
			
		||||
| 
						 | 
				
			
			@ -401,10 +677,200 @@ func LookupEnvOrInt(key string, defaultVal int) int {
 | 
			
		|||
	return defaultVal
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCredVar reads value from environment variable or returns fallback
 | 
			
		||||
func GetCredVar(key, fallback string) string {
 | 
			
		||||
	if value, ok := os.LookupEnv(key); ok {
 | 
			
		||||
		return value
 | 
			
		||||
func LookupEnvOrStrings(key string, defaultVal []string) []string {
 | 
			
		||||
	if val, ok := os.LookupEnv(key); ok {
 | 
			
		||||
		return strings.Split(val, ",")
 | 
			
		||||
	}
 | 
			
		||||
	return fallback
 | 
			
		||||
	return defaultVal
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LookupEnvOrFile(key string, defaultVal string) string {
 | 
			
		||||
	if val, ok := os.LookupEnv(key); ok {
 | 
			
		||||
		if file, err := os.Open(val); err == nil {
 | 
			
		||||
			var content string
 | 
			
		||||
			scanner := bufio.NewScanner(file)
 | 
			
		||||
			for scanner.Scan() {
 | 
			
		||||
				content += scanner.Text()
 | 
			
		||||
			}
 | 
			
		||||
			return content
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defaultVal
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func StringFromEmbedFile(embed fs.FS, filename string) (string, error) {
 | 
			
		||||
	file, err := embed.Open(filename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	content, err := io.ReadAll(file)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return string(content), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseLogLevel(lvl string) (log.Lvl, error) {
 | 
			
		||||
	switch strings.ToLower(lvl) {
 | 
			
		||||
	case "debug":
 | 
			
		||||
		return log.DEBUG, nil
 | 
			
		||||
	case "info":
 | 
			
		||||
		return log.INFO, nil
 | 
			
		||||
	case "warn":
 | 
			
		||||
		return log.WARN, nil
 | 
			
		||||
	case "error":
 | 
			
		||||
		return log.ERROR, nil
 | 
			
		||||
	case "off":
 | 
			
		||||
		return log.OFF, nil
 | 
			
		||||
	default:
 | 
			
		||||
		return log.DEBUG, fmt.Errorf("not a valid log level: %s", lvl)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCurrentHash returns current hashes
 | 
			
		||||
func GetCurrentHash(db store.IStore) (string, string) {
 | 
			
		||||
	hashClients, _ := dirhash.HashDir(path.Join(db.GetPath(), "clients"), "prefix", dirhash.Hash1)
 | 
			
		||||
	files := append([]string(nil), "prefix/global_settings.json", "prefix/interfaces.json", "prefix/keypair.json")
 | 
			
		||||
 | 
			
		||||
	osOpen := func(name string) (io.ReadCloser, error) {
 | 
			
		||||
		return os.Open(filepath.Join(path.Join(db.GetPath(), "server"), strings.TrimPrefix(name, "prefix")))
 | 
			
		||||
	}
 | 
			
		||||
	hashServer, _ := dirhash.Hash1(files, osOpen)
 | 
			
		||||
 | 
			
		||||
	return hashClients, hashServer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func HashesChanged(db store.IStore) bool {
 | 
			
		||||
	old, _ := db.GetHashes()
 | 
			
		||||
	oldClient := old.Client
 | 
			
		||||
	oldServer := old.Server
 | 
			
		||||
	newClient, newServer := GetCurrentHash(db)
 | 
			
		||||
 | 
			
		||||
	if oldClient != newClient {
 | 
			
		||||
		//fmt.Println("Hash for client differs")
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	if oldServer != newServer {
 | 
			
		||||
		//fmt.Println("Hash for server differs")
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UpdateHashes(db store.IStore) error {
 | 
			
		||||
	var clientServerHashes model.ClientServerHashes
 | 
			
		||||
	clientServerHashes.Client, clientServerHashes.Server = GetCurrentHash(db)
 | 
			
		||||
	return db.SaveHashes(clientServerHashes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RandomString(length int) string {
 | 
			
		||||
	var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
 | 
			
		||||
	charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
 | 
			
		||||
	b := make([]byte, length)
 | 
			
		||||
	for i := range b {
 | 
			
		||||
		b[i] = charset[seededRand.Intn(len(charset))]
 | 
			
		||||
	}
 | 
			
		||||
	return string(b)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ManagePerms(path string) error {
 | 
			
		||||
	err := os.Chmod(path, 0600)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func AddTgToClientID(userid int64, clientID string) {
 | 
			
		||||
	TgUseridToClientIDMutex.Lock()
 | 
			
		||||
	defer TgUseridToClientIDMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	if _, ok := TgUseridToClientID[userid]; ok && TgUseridToClientID[userid] != nil {
 | 
			
		||||
		TgUseridToClientID[userid] = append(TgUseridToClientID[userid], clientID)
 | 
			
		||||
	} else {
 | 
			
		||||
		TgUseridToClientID[userid] = []string{clientID}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UpdateTgToClientID(userid int64, clientID string) {
 | 
			
		||||
	TgUseridToClientIDMutex.Lock()
 | 
			
		||||
	defer TgUseridToClientIDMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	// Detach clientID from any existing userid
 | 
			
		||||
	for uid, cls := range TgUseridToClientID {
 | 
			
		||||
		if cls != nil {
 | 
			
		||||
			filtered := filterStringSlice(cls, clientID)
 | 
			
		||||
			if len(filtered) > 0 {
 | 
			
		||||
				TgUseridToClientID[uid] = filtered
 | 
			
		||||
			} else {
 | 
			
		||||
				delete(TgUseridToClientID, uid)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Attach it to the new one
 | 
			
		||||
	if _, ok := TgUseridToClientID[userid]; ok && TgUseridToClientID[userid] != nil {
 | 
			
		||||
		TgUseridToClientID[userid] = append(TgUseridToClientID[userid], clientID)
 | 
			
		||||
	} else {
 | 
			
		||||
		TgUseridToClientID[userid] = []string{clientID}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RemoveTgToClientID(clientID string) {
 | 
			
		||||
	TgUseridToClientIDMutex.Lock()
 | 
			
		||||
	defer TgUseridToClientIDMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	// Detach clientID from any existing userid
 | 
			
		||||
	for uid, cls := range TgUseridToClientID {
 | 
			
		||||
		if cls != nil {
 | 
			
		||||
			filtered := filterStringSlice(cls, clientID)
 | 
			
		||||
			if len(filtered) > 0 {
 | 
			
		||||
				TgUseridToClientID[uid] = filtered
 | 
			
		||||
			} else {
 | 
			
		||||
				delete(TgUseridToClientID, uid)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func filterStringSlice(s []string, excludedStr string) []string {
 | 
			
		||||
	filtered := s[:0]
 | 
			
		||||
	for _, v := range s {
 | 
			
		||||
		if v != excludedStr {
 | 
			
		||||
			filtered = append(filtered, v)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return filtered
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetDBUserCRC32(dbuser model.User) uint32 {
 | 
			
		||||
	buf := new(bytes.Buffer)
 | 
			
		||||
	enc := gob.NewEncoder(buf)
 | 
			
		||||
	if err := enc.Encode(dbuser); err != nil {
 | 
			
		||||
		panic("model.User is gob-incompatible, session verification is impossible")
 | 
			
		||||
	}
 | 
			
		||||
	return crc32.ChecksumIEEE(buf.Bytes())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ConcatMultipleSlices(slices ...[]byte) []byte {
 | 
			
		||||
	var totalLen int
 | 
			
		||||
 | 
			
		||||
	for _, s := range slices {
 | 
			
		||||
		totalLen += len(s)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result := make([]byte, totalLen)
 | 
			
		||||
 | 
			
		||||
	var i int
 | 
			
		||||
 | 
			
		||||
	for _, s := range slices {
 | 
			
		||||
		i += copy(result[i:], s)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetCookiePath() string {
 | 
			
		||||
	cookiePath := BasePath
 | 
			
		||||
	if cookiePath == "" {
 | 
			
		||||
		cookiePath = "/"
 | 
			
		||||
	}
 | 
			
		||||
	return cookiePath
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue