Compare commits
	
		
			116 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 | 
|  | @ -25,3 +25,6 @@ docker-compose* | ||||||
| db | db | ||||||
| assets | assets | ||||||
| wireguard-ui | wireguard-ui | ||||||
|  | 
 | ||||||
|  | # Examples | ||||||
|  | examples | ||||||
|  |  | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | *.html linguist-detectable=false | ||||||
|  | @ -39,7 +39,7 @@ markComment: > | ||||||
| limitPerRun: 30 | limitPerRun: 30 | ||||||
| 
 | 
 | ||||||
| # Limit to only `issues` or `pulls` | # Limit to only `issues` or `pulls` | ||||||
| # only: issues | only: issues | ||||||
| 
 | 
 | ||||||
| # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': | ||||||
| # pulls: | # pulls: | ||||||
|  |  | ||||||
|  | @ -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: | jobs: | ||||||
|   releases-matrix: |   releases-matrix: | ||||||
|     name: Release Go Binary |     name: Release Go Binary | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-22.04 | ||||||
|     strategy: |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         # build and publish in parallel: linux/386, linux/amd64, darwin/386, darwin/amd64 |         # build and publish in parallel: linux/386, linux/amd64, darwin/386, darwin/amd64 | ||||||
|         goos: [linux, freebsd, darwin] |         goos: [linux, freebsd, darwin] | ||||||
|  | @ -24,7 +25,7 @@ jobs: | ||||||
|           - 7 |           - 7 | ||||||
|     steps: |     steps: | ||||||
|     # get the source code |     # get the source code | ||||||
|     - uses: actions/checkout@v2 |     - uses: actions/checkout@v4 | ||||||
| 
 | 
 | ||||||
|     # set environment |     # set environment | ||||||
|     - name: Set APP_VERSION env |     - name: Set APP_VERSION env | ||||||
|  | @ -35,25 +36,17 @@ jobs: | ||||||
|       uses: managedkaos/print-env@v1.0 |       uses: managedkaos/print-env@v1.0 | ||||||
| 
 | 
 | ||||||
|     # setup node |     # setup node | ||||||
|     - uses: actions/setup-node@v2 |     - uses: actions/setup-node@v4 | ||||||
|       with: |       with: | ||||||
|         node-version: '14' |         node-version: '20' | ||||||
|         registry-url: 'https://registry.npmjs.org' |         registry-url: 'https://registry.npmjs.org' | ||||||
| 
 | 
 | ||||||
|     # prepare assets for go rice |     # prepare assets | ||||||
|     - name: Prepare assets |     - name: Prepare assets | ||||||
|       run: | |       run: | | ||||||
|         chmod +x ./prepare_assets.sh |         chmod +x ./prepare_assets.sh | ||||||
|         ./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 |     # build and make the releases | ||||||
|     - name: Build and make the releases |     - name: Build and make the releases | ||||||
|       uses: wangyoucao577/go-release-action@master |       uses: wangyoucao577/go-release-action@master | ||||||
|  | @ -61,7 +54,8 @@ jobs: | ||||||
|         github_token: ${{ secrets.GITHUB_TOKEN }} |         github_token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|         goos: ${{ matrix.goos }} |         goos: ${{ matrix.goos }} | ||||||
|         goarch: ${{ matrix.goarch }} |         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" |         binary_name: "wireguard-ui" | ||||||
|         build_flags: -v |         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 }} |         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) | # Dependency directories and files (remove the comment below to include it) | ||||||
| vendor/ | vendor/ | ||||||
| assets/ | assets/* | ||||||
|  | !assets/.gitkeep | ||||||
| node_modules/ | node_modules/ | ||||||
| rice-box.go |  | ||||||
| 
 | 
 | ||||||
| # IDEs | # IDEs | ||||||
| .vscode | .vscode | ||||||
| .idea | .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,9 +1,13 @@ | ||||||
| # Build stage | # Build stage | ||||||
| FROM golang:1.16.7-alpine3.14 as builder | FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.21-alpine3.19 AS builder | ||||||
| LABEL maintainer="Khanh Ngo <k@ndk.name" | LABEL maintainer="Khanh Ngo <k@ndk.name>" | ||||||
| 
 | 
 | ||||||
| ARG TARGETOS=linux | ARG BUILDPLATFORM | ||||||
| ARG TARGETARCH=amd64 | ARG TARGETOS | ||||||
|  | ARG TARGETARCH | ||||||
|  | ARG APP_VERSION=dev | ||||||
|  | ARG BUILD_TIME | ||||||
|  | ARG GIT_COMMIT | ||||||
| 
 | 
 | ||||||
| ARG BUILD_DEPENDENCIES="npm \ | ARG BUILD_DEPENDENCIES="npm \ | ||||||
|     yarn" |     yarn" | ||||||
|  | @ -13,8 +17,11 @@ RUN apk add --update --no-cache ${BUILD_DEPENDENCIES} | ||||||
| 
 | 
 | ||||||
| WORKDIR /build | WORKDIR /build | ||||||
| 
 | 
 | ||||||
| # Add sources | # Add dependencies | ||||||
| COPY . /build | COPY go.mod /build | ||||||
|  | COPY go.sum /build | ||||||
|  | COPY package.json /build | ||||||
|  | COPY yarn.lock /build | ||||||
| 
 | 
 | ||||||
| # Prepare assets | # Prepare assets | ||||||
| RUN yarn install --pure-lockfile --production && \ | RUN yarn install --pure-lockfile --production && \ | ||||||
|  | @ -39,34 +46,32 @@ RUN mkdir -p assets/plugins && \ | ||||||
|     /build/node_modules/jquery-tags-input/ \ |     /build/node_modules/jquery-tags-input/ \ | ||||||
|     assets/plugins/ |     assets/plugins/ | ||||||
| 
 | 
 | ||||||
|  | # Add sources | ||||||
|  | COPY . /build | ||||||
|  | 
 | ||||||
| # Move custom assets | # Move custom assets | ||||||
| RUN cp -r /build/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 | # Build | ||||||
| RUN rice embed-go && \ | 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 . | ||||||
|     CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -a -o wg-ui . |  | ||||||
| 
 | 
 | ||||||
| # Release stage | # Release stage | ||||||
| FROM alpine:3.11 | FROM alpine:3.19 | ||||||
| 
 | 
 | ||||||
| RUN addgroup -S wgui && \ | RUN addgroup -S wgui && \ | ||||||
|     adduser -S -D -G wgui 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 | WORKDIR /app | ||||||
| 
 | 
 | ||||||
| RUN mkdir -p db | RUN mkdir -p db | ||||||
| 
 | 
 | ||||||
| # Copy binary files | # 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 | RUN chmod +x wg-ui | ||||||
|  | COPY init.sh . | ||||||
|  | RUN chmod +x init.sh | ||||||
| 
 | 
 | ||||||
| EXPOSE 5000/tcp | EXPOSE 5000/tcp | ||||||
| HEALTHCHECK CMD ["wget","--output-document=-","--quiet","--tries=1","http://127.0.0.1:5000/_health"] | ENTRYPOINT ["./init.sh"] | ||||||
| ENTRYPOINT ["./wg-ui"] |  | ||||||
|  |  | ||||||
|  | @ -1,2 +0,0 @@ | ||||||
| @Library('ndk-jenkins-shared-libs')_ |  | ||||||
| imageBuilder('wireguard-ui') |  | ||||||
							
								
								
									
										222
									
								
								README.md
								
								
								
								
							
							
						
						
									
										222
									
								
								README.md
								
								
								
								
							|  | @ -5,72 +5,119 @@ | ||||||
| A web user interface to manage your WireGuard setup. | A web user interface to manage your WireGuard setup. | ||||||
| 
 | 
 | ||||||
| ## Features | ## Features | ||||||
|  | 
 | ||||||
| - Friendly UI | - Friendly UI | ||||||
| - Authentication | - Authentication | ||||||
| - Manage extra client's information (name, email, etc) | - Manage extra client information (name, email, etc.) | ||||||
| - Retrieve configs using QR code / file | - Retrieve client config using QR code / file / email / Telegram | ||||||
|  | 
 | ||||||
|  |  | ||||||
| 
 | 
 | ||||||
| ## Run WireGuard-UI | ## Run WireGuard-UI | ||||||
| 
 | 
 | ||||||
| Default username and password are `admin`. | > ⚠️The default username and password are `admin`. Please change it to secure your setup. | ||||||
| 
 |  | ||||||
| ### 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, set the following environment variables: |  | ||||||
| 
 |  | ||||||
| - using SendGrid API |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| 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 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| - using SMTP |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| SMTP_HOSTNAME |  | ||||||
| SMTP_PORT |  | ||||||
| SMTP_USERNAME |  | ||||||
| SMTP_PASSWORD |  | ||||||
| SMTP_AUTH_TYPE |  | ||||||
| EMAIL_FROM_ADDRESS: the sender's email address |  | ||||||
| EMAIL_FROM_NAME: the sender's name |  | ||||||
| ``` |  | ||||||
| 
 | 
 | ||||||
| ### Using binary file | ### 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 | ./wireguard-ui | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Auto restart WireGuard daemon | ### Using docker compose | ||||||
| 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: |  | ||||||
| 
 | 
 | ||||||
| ### systemd | 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: | ||||||
| Create /etc/systemd/system/wgui.service |  | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
|  | 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] | [Unit] | ||||||
| Description=Restart WireGuard | Description=Restart WireGuard | ||||||
| After=network.target | After=network.target | ||||||
|  | @ -81,11 +128,14 @@ ExecStart=/usr/bin/systemctl restart wg-quick@wg0.service | ||||||
| 
 | 
 | ||||||
| [Install] | [Install] | ||||||
| RequiredBy=wgui.path | 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] | [Unit] | ||||||
| Description=Watch /etc/wireguard/wg0.conf for changes | Description=Watch /etc/wireguard/wg0.conf for changes | ||||||
| 
 | 
 | ||||||
|  | @ -94,81 +144,103 @@ PathModified=/etc/wireguard/wg0.conf | ||||||
| 
 | 
 | ||||||
| [Install] | [Install] | ||||||
| WantedBy=multi-user.target | WantedBy=multi-user.target | ||||||
|  | EOF | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Apply it | Apply it | ||||||
| 
 | 
 | ||||||
| ``` | ```sh | ||||||
| systemctl enable wgui.{path,service} | systemctl enable wgui.{path,service} | ||||||
| systemctl start 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 | #!/bin/sh | ||||||
| wg-quick down wg0 | wg-quick down wg0 | ||||||
| wg-quick up 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 | #!/sbin/openrc-run | ||||||
| 
 | 
 | ||||||
| command=/sbin/inotifyd | command=/sbin/inotifyd | ||||||
| command_args="/usr/local/bin/wgui /etc/wireguard/wg0.conf:w" | command_args="/usr/local/bin/wgui /etc/wireguard/wg0.conf:w" | ||||||
| pidfile=/run/${RC_SVCNAME}.pid | pidfile=/run/${RC_SVCNAME}.pid | ||||||
| command_background=yes | command_background=yes | ||||||
|  | EOF | ||||||
|  | chmod +x wgui | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Apply it | Apply it | ||||||
| 
 | 
 | ||||||
| ``` | ```sh | ||||||
| rc-service wgui start | rc-service wgui start | ||||||
| rc-update add wgui default | 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 | ||||||
| 
 | 
 | ||||||
| ### Build docker image | ### Build docker image | ||||||
| 
 | 
 | ||||||
| Go to the project root directory and run the following command: | 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 | ### Build binary file | ||||||
| 
 | 
 | ||||||
| Prepare the assets directory | Prepare the assets directory | ||||||
| 
 | 
 | ||||||
| ``` | ```sh | ||||||
| ./prepare_assets.sh | ./prepare_assets.sh | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Then you can embed resources by generating Go source code | Then build your executable | ||||||
| 
 | 
 | ||||||
| ``` | ```sh | ||||||
| rice embed-go |  | ||||||
| go build -o wireguard-ui | 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 | ## License | ||||||
|  | 
 | ||||||
| MIT. See [LICENSE](https://github.com/ngoduykhanh/wireguard-ui/blob/master/LICENSE). | MIT. See [LICENSE](https://github.com/ngoduykhanh/wireguard-ui/blob/master/LICENSE). | ||||||
| 
 | 
 | ||||||
| ## Support | ## Support | ||||||
|  | 
 | ||||||
| If you like the project and want to support it, you can *buy me a coffee* ☕ | 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> | <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) { | function renderClientList(data) { | ||||||
|     $.each(data, function(index, obj) { |     $.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
 |         // render client status css tag style
 | ||||||
|         let clientStatusHtml = '>' |         let clientStatusHtml = '>' | ||||||
|         if (obj.Client.enabled) { |         if (obj.Client.enabled) { | ||||||
|  | @ -18,15 +33,25 @@ function renderClientList(data) { | ||||||
|             allowedIpsHtml += `<small class="badge badge-secondary">${obj}</small> `; |             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
 |         // render client html content
 | ||||||
|         let html = `<div class="col-sm-6 col-md-6 col-lg-4" 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="info-box"> | ||||||
|                             <div class="overlay" id="paused_${obj.Client.id}"` + clientStatusHtml
 |                             <div class="overlay" id="paused_${obj.Client.id}"` + clientStatusHtml
 | ||||||
|                                 + `<i class="paused-client fas fa-3x fa-play" onclick="resumeClient('${obj.Client.id}')"></i>
 |                                 + `<i class="paused-client fas fa-3x fa-play" onclick="resumeClient('${obj.Client.id}')"></i>
 | ||||||
|                             </div> |                             </div> | ||||||
|                             <div class="info-box-content"> |                             <div class="info-box-content" style="overflow: hidden"> | ||||||
|                                 <div class="btn-group"> |                                 <div class="btn-group"> | ||||||
|                                     <a href="/download?clientid=${obj.Client.id}" class="btn btn-outline-primary btn-sm">Download</a> |                                     <a href="download?clientid=${obj.Client.id}" class="btn btn-outline-primary btn-sm">Download</a> | ||||||
|                                 </div> |                                 </div> | ||||||
|                                 <div class="btn-group">       |                                 <div class="btn-group">       | ||||||
|                                     <button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" |                                     <button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" | ||||||
|  | @ -38,7 +63,7 @@ function renderClientList(data) { | ||||||
|                                         data-target="#modal_email_client" data-clientid="${obj.Client.id}" |                                         data-target="#modal_email_client" data-clientid="${obj.Client.id}" | ||||||
|                                         data-clientname="${obj.Client.name}">Email</button> |                                         data-clientname="${obj.Client.name}">Email</button> | ||||||
|                                 </div> |                                 </div> | ||||||
| 
 |                                 ${telegramButton} | ||||||
|                                 <div class="btn-group"> |                                 <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">More</button> | ||||||
|                                     <button type="button" class="btn btn-outline-danger btn-sm dropdown-toggle dropdown-icon"  |                                     <button type="button" class="btn btn-outline-danger btn-sm dropdown-toggle dropdown-icon"  | ||||||
|  | @ -58,6 +83,10 @@ function renderClientList(data) { | ||||||
|                                 </div> |                                 </div> | ||||||
|                                 <hr> |                                 <hr> | ||||||
|                                 <span class="info-box-text"><i class="fas fa-user"></i> ${obj.Client.name}</span> |                                 <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-envelope"></i> ${obj.Client.email}</span> | ||||||
|                                 <span class="info-box-text"><i class="fas fa-clock"></i> |                                 <span class="info-box-text"><i class="fas fa-clock"></i> | ||||||
|                                     ${prettyDateTime(obj.Client.created_at)}</span> |                                     ${prettyDateTime(obj.Client.created_at)}</span> | ||||||
|  | @ -65,6 +94,8 @@ function renderClientList(data) { | ||||||
|                                     ${prettyDateTime(obj.Client.updated_at)}</span> |                                     ${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> |                                 <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> |                                     ${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>` |                                 <span class="info-box-text"><strong>IP Allocation</strong></span>` | ||||||
|                                 + allocatedIpsHtml |                                 + allocatedIpsHtml | ||||||
|                                 + `<span class="info-box-text"><strong>Allowed IPs</strong></span>` |                                 + `<span class="info-box-text"><strong>Allowed IPs</strong></span>` | ||||||
|  | @ -78,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) { | function prettyDateTime(timeStr) { | ||||||
|     const dt = new Date(timeStr); |     const dt = new Date(timeStr); | ||||||
|     const offsetMs = dt.getTimezoneOffset() * 60 * 1000; |     const offsetMs = dt.getTimezoneOffset() * 60 * 1000; | ||||||
|  |  | ||||||
|  | @ -1,3 +1,8 @@ | ||||||
|  | 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' + | const wake_on_lan_new_template = '<div class="col-sm-4" id="{{ .Id }}">\n' + | ||||||
|     '\t<div class="info-box">\n' + |     '\t<div class="info-box">\n' + | ||||||
|     '\t\t<div class="info-box-content">\n' + |     '\t\t<div class="info-box-content">\n' + | ||||||
|  | @ -24,9 +29,9 @@ const wake_on_lan_new_template = '<div class="col-sm-4" id="{{ .Id }}">\n' + | ||||||
|     '</div>'; |     '</div>'; | ||||||
| 
 | 
 | ||||||
| jQuery(function ($) { | jQuery(function ($) { | ||||||
|    $.validator.addMethod('mac', function (value, element) { |     $.validator.addMethod('mac', function (value, element) { | ||||||
|        return this.optional(element) || /^([0-9A-F]{2}[:]){5}([0-9A-F]{2})$/.test(value); |         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'); |     }, '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.each(["put", "delete"], function (i, method) { | ||||||
|  | @ -42,7 +47,8 @@ jQuery.each(["put", "delete"], function (i, method) { | ||||||
|             type: method, |             type: method, | ||||||
|             dataType: type, |             dataType: type, | ||||||
|             data: data, |             data: data, | ||||||
|             success: callback |             success: callback, | ||||||
|  |             contentType: 'application/json' | ||||||
|         }); |         }); | ||||||
|     }; |     }; | ||||||
| }); | }); | ||||||
|  | @ -55,7 +61,7 @@ jQuery(function ($) { | ||||||
| jQuery(function ($) { | jQuery(function ($) { | ||||||
|     $('.btn-outline-success').click(function () { |     $('.btn-outline-success').click(function () { | ||||||
|         const $this = $(this); |         const $this = $(this); | ||||||
|         $.put('/wake_on_lan_host/' + $this.data('mac-address'), function (result) { |         $.put(base_url + 'wake_on_lan_host/' + $this.data('mac-address'), function (result) { | ||||||
|             $this.parents('.info-box').find('.latest-used').text(prettyDateTime(result)); |             $this.parents('.info-box').find('.latest-used').text(prettyDateTime(result)); | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
|  | @ -76,7 +82,7 @@ jQuery(function ($) { | ||||||
| 
 | 
 | ||||||
|     $remove_client_confirm.click(function () { |     $remove_client_confirm.click(function () { | ||||||
|         const macAddress = $remove_client_confirm.val().replaceAll(":", "-"); |         const macAddress = $remove_client_confirm.val().replaceAll(":", "-"); | ||||||
|         $.delete('/wake_on_lan_host/' + macAddress); |         $.delete(base_url + 'wake_on_lan_host/' + macAddress); | ||||||
|         $('#' + macAddress).remove(); |         $('#' + macAddress).remove(); | ||||||
| 
 | 
 | ||||||
|         $modal_remove_wake_on_lan_host.modal('hide'); |         $modal_remove_wake_on_lan_host.modal('hide'); | ||||||
|  | @ -84,10 +90,18 @@ jQuery(function ($) { | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| jQuery(function ($) { | jQuery(function ($) { | ||||||
|    $('.latest-used').each(function () { |     $('.latest-used').each(function () { | ||||||
|        const $this = $(this); |         const $this = $(this); | ||||||
|        $this.text(prettyDateTime($this.text().trim())); |         const timeText = $this.text().trim(); | ||||||
|    }); |         try { | ||||||
|  |             if (timeText != "Unused") { | ||||||
|  |                 $this.text(prettyDateTime(timeText)); | ||||||
|  |             } | ||||||
|  |         } catch (ex) { | ||||||
|  |             console.log(timeText); | ||||||
|  |             throw ex; | ||||||
|  |         } | ||||||
|  |     }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| jQuery(function ($) { | jQuery(function ($) { | ||||||
|  | @ -109,7 +123,7 @@ jQuery(function ($) { | ||||||
|             $.ajax({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'POST', |                 method: 'POST', | ||||||
|                 url: '/wake_on_lan_host', |                 url: base_url + 'wake_on_lan_host', | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 data: JSON.stringify(data), |                 data: JSON.stringify(data), | ||||||
|  |  | ||||||
|  | @ -15,6 +15,9 @@ services: | ||||||
|       - SESSION_SECRET |       - SESSION_SECRET | ||||||
|       - WGUI_USERNAME=alpha |       - WGUI_USERNAME=alpha | ||||||
|       - WGUI_PASSWORD=this-unusual-password |       - WGUI_PASSWORD=this-unusual-password | ||||||
|  |       - WG_CONF_TEMPLATE | ||||||
|  |       - WGUI_MANAGE_START=false | ||||||
|  |       - WGUI_MANAGE_RESTART=false | ||||||
|     logging: |     logging: | ||||||
|       driver: json-file |       driver: json-file | ||||||
|       options: |       options: | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ package emailer | ||||||
| import ( | import ( | ||||||
| 	"crypto/tls" | 	"crypto/tls" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	mail "github.com/xhit/go-simple-mail/v2" | 	mail "github.com/xhit/go-simple-mail/v2" | ||||||
|  | @ -13,14 +14,16 @@ type SmtpMail struct { | ||||||
| 	port       int | 	port       int | ||||||
| 	username   string | 	username   string | ||||||
| 	password   string | 	password   string | ||||||
|  | 	smtpHelo   string | ||||||
| 	authType   mail.AuthType | 	authType   mail.AuthType | ||||||
|  | 	encryption mail.Encryption | ||||||
| 	noTLSCheck bool | 	noTLSCheck bool | ||||||
| 	fromName   string | 	fromName   string | ||||||
| 	from       string | 	from       string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func authType(authType string) mail.AuthType { | func authType(authType string) mail.AuthType { | ||||||
| 	switch authType { | 	switch strings.ToUpper(authType) { | ||||||
| 	case "PLAIN": | 	case "PLAIN": | ||||||
| 		return mail.AuthPlain | 		return mail.AuthPlain | ||||||
| 	case "LOGIN": | 	case "LOGIN": | ||||||
|  | @ -30,8 +33,23 @@ func authType(authType string) mail.AuthType { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewSmtpMail(hostname string, port int, username string, password string, noTLSCheck bool, auth string, fromName, from string) *SmtpMail { | func encryptionType(encryptionType string) mail.Encryption { | ||||||
| 	ans := SmtpMail{hostname: hostname, port: port, username: username, password: password, noTLSCheck: noTLSCheck, fromName: fromName, from: from, authType: authType(auth)} | 	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 | 	return &ans | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -50,7 +68,8 @@ func (o *SmtpMail) Send(toName string, to string, subject string, content string | ||||||
| 	server.Authentication = o.authType | 	server.Authentication = o.authType | ||||||
| 	server.Username = o.username | 	server.Username = o.username | ||||||
| 	server.Password = o.password | 	server.Password = o.password | ||||||
| 	server.Encryption = mail.EncryptionSTARTTLS | 	server.Helo = o.smtpHelo | ||||||
|  | 	server.Encryption = o.encryption | ||||||
| 	server.KeepAlive = false | 	server.KeepAlive = false | ||||||
| 	server.ConnectTimeout = 10 * time.Second | 	server.ConnectTimeout = 10 * time.Second | ||||||
| 	server.SendTimeout = 10 * time.Second | 	server.SendTimeout = 10 * time.Second | ||||||
|  |  | ||||||
|  | @ -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,26 +1,52 @@ | ||||||
| module github.com/ngoduykhanh/wireguard-ui | module github.com/ngoduykhanh/wireguard-ui | ||||||
| 
 | 
 | ||||||
| go 1.14 | go 1.21 | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
| 	github.com/GeertJohan/go.rice v1.0.0 | 	github.com/NicoNex/echotron/v3 v3.27.0 | ||||||
| 	github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd | 	github.com/glendc/go-external-ip v0.1.0 | ||||||
| 	github.com/go-playground/universal-translator v0.17.0 // indirect | 	github.com/gorilla/sessions v1.2.2 | ||||||
| 	github.com/gorilla/sessions v1.2.0 | 	github.com/labstack/echo-contrib v0.15.0 | ||||||
| 	github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 // indirect | 	github.com/labstack/echo/v4 v4.11.4 | ||||||
| 	github.com/labstack/echo-contrib v0.9.0 | 	github.com/labstack/gommon v0.4.2 | ||||||
| 	github.com/labstack/echo/v4 v4.1.16 | 	github.com/rs/xid v1.5.0 | ||||||
| 	github.com/labstack/gommon v0.3.0 | 	github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d | ||||||
| 	github.com/leodido/go-urn v1.2.0 // indirect | 	github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8 | ||||||
| 	github.com/rs/xid v1.2.1 | 	github.com/sendgrid/sendgrid-go v3.14.0+incompatible | ||||||
| 	github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba | 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e | ||||||
| 	github.com/sendgrid/rest v2.6.4+incompatible // indirect | 	github.com/xhit/go-simple-mail/v2 v2.16.0 | ||||||
| 	github.com/sendgrid/sendgrid-go v3.10.0+incompatible | 	golang.org/x/crypto v0.17.0 | ||||||
| 	github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 | 	golang.org/x/mod v0.14.0 | ||||||
| 	github.com/xhit/go-simple-mail/v2 v2.10.0 |  | ||||||
| 	//golang.zx2c4.com/wireguard v0.0.20200121 // indirect | 	//golang.zx2c4.com/wireguard v0.0.20200121 // indirect | ||||||
| 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c | 	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 | 	gopkg.in/go-playground/validator.v9 v9.31.0 | ||||||
| 	github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d | ) | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
| ) | ) | ||||||
|  |  | ||||||
							
								
								
									
										228
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										228
									
								
								go.sum
								
								
								
								
							|  | @ -1,59 +1,41 @@ | ||||||
| github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= | github.com/NicoNex/echotron/v3 v3.27.0 h1:iq4BLPO+Dz1JHjh2HPk0D0NldAZSYcAjaOicgYEhUzw= | ||||||
| github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ= | github.com/NicoNex/echotron/v3 v3.27.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI= | ||||||
| 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/coreos/bbolt v1.3.1-coreos.6.0.20180223184059-4f5275f4ebbf/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= | github.com/coreos/bbolt v1.3.1-coreos.6.0.20180223184059-4f5275f4ebbf/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= | ||||||
| github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY= |  | ||||||
| github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= |  | ||||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.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/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/glendc/go-external-ip v0.1.0 h1:iX3xQ2Q26atAmLTbd++nUce2P5ht5P4uD4V7caSY/xg= | ||||||
| github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | github.com/glendc/go-external-ip v0.1.0/go.mod h1:CNx312s2FLAJoWNdJWZ2Fpf5O4oLsMFwuYviHjS4uJE= | ||||||
| github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd h1:1BzxHapafGJd/XlpMvocLeDBin2EKn90gXv2AQt5sfo= | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | ||||||
| github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd/go.mod h1:o9OoDQyE1WHvYVUH1FdFapy1/rCZHHq3O5wS4VA83ig= | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||||||
| github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | ||||||
| github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||||
| github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= | github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= | ||||||
| github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= | github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= | ||||||
| github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= | ||||||
| github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= | ||||||
| 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/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= | 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.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.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.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.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
| github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= |  | ||||||
| github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||||||
| github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||||
| github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | ||||||
| github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
| github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= | ||||||
| github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= | github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= | ||||||
| github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= | ||||||
| github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | 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 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8= | ||||||
| github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw= | github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw= | ||||||
| github.com/jessevdk/go-flags v0.0.0-20150816100521-1acbbaff2f34/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= | github.com/jessevdk/go-flags v0.0.0-20150816100521-1acbbaff2f34/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= | ||||||
| github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= |  | ||||||
| github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA= |  | ||||||
| github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= | github.com/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-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-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-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= | ||||||
|  | @ -61,34 +43,26 @@ github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR7 | ||||||
| github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs= | 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-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA= | ||||||
| github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U= | 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/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU= | ||||||
| github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4= | ||||||
| github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= | github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= | ||||||
| github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= | ||||||
| github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= | ||||||
| github.com/labstack/echo-contrib v0.9.0 h1:hKBA2SnxdxR7sghH0J04zq/pImnKRmgvmQ6MvY9hug4= | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= | ||||||
| github.com/labstack/echo-contrib v0.9.0/go.mod h1:TsFE5Vv0LRpZLoh4mMmaaAxzcTH+1CBFiUtVhwlegzU= | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= | ||||||
| github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE= | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= | ||||||
| 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/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= |  | ||||||
| github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= |  | ||||||
| github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= | ||||||
| github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= | ||||||
| github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= | 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/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | ||||||
| github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= |  | ||||||
| github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | ||||||
| github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= | 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/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.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 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.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= | ||||||
| github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= | github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= | ||||||
|  | @ -97,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.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= | ||||||
| github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/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.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.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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | 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/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= | ||||||
| github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | ||||||
| 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/sabhiram/go-colorize v0.0.0-20210403184538-366f55d711cf/go.mod h1:GvlEbMJBpbAXFn06UajbdBlGZ18iLvHyuIrgG//L8uk= | 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 h1:NDtoSmsxTpDYTqvUurn2ooAzDaYbJSB9/tOhLzaewgo= | ||||||
| github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d/go.mod h1:SVPBBd492Gk7Cq5lPd6OAYtIGk2r1FsyH8KT3IB8h7c= | github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d/go.mod h1:SVPBBd492Gk7Cq5lPd6OAYtIGk2r1FsyH8KT3IB8h7c= | ||||||
| github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba h1:8QAc9wFAf2b/9cAXskm0wBylObZ0bTpRcaP7ThjLPVQ= | github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8 h1:hlNRl87eAZhh2QMJVShuXHL6OOd0ObZM0JozDIruNeM= | ||||||
| github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba/go.mod h1:W6zxGUBCXRR5QugSd/nFcFVmwoGnvpjiNY/JwT03Wew= | github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8/go.mod h1:W6zxGUBCXRR5QugSd/nFcFVmwoGnvpjiNY/JwT03Wew= | ||||||
| github.com/sendgrid/rest v2.6.4+incompatible h1:lq6gAQxLwVBf3mVyCCSHI6mgF+NfaJFJHjT0kl6SSo8= | github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= | ||||||
| github.com/sendgrid/rest v2.6.4+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= | github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= | ||||||
| github.com/sendgrid/sendgrid-go v3.10.0+incompatible h1:aSYyurHxEZSDy7kxhvZ4fH0inNkEEmRssZNbAmETR2c= | github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA= | ||||||
| github.com/sendgrid/sendgrid-go v3.10.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= | github.com/sendgrid/sendgrid-go v3.14.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-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= | ||||||
| github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= | ||||||
| github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= |  | ||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||||
| github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= | 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 v0.0.0-20150929183540-2b15294402a8/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||||||
| github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||||
| github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | ||||||
| github.com/uber/jaeger-client-go v2.19.1-0.20191002155754-0be28c34dabf+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||||
| github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= | 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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= | ||||||
| github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= | 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.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= | ||||||
| github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= | ||||||
| github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= | github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA= | ||||||
| github.com/xhit/go-simple-mail/v2 v2.10.0 h1:nib6RaJ4qVh5HD9UE9QJqnUZyWp3upv+Z6CFxaMj0V8= | github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98= | ||||||
| github.com/xhit/go-simple-mail/v2 v2.10.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4= |  | ||||||
| go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= |  | ||||||
| golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= |  | ||||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | 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-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-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-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= | ||||||
| golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e h1:8foAy0aoO5GkqCvAEJ4VC4P3zksTg4X4aJCDpZzmgQI= |  | ||||||
| golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= | golang.org/x/crypto v0.0.0-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-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-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-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
| golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | golang.org/x/net v0.0.0-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-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-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-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-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-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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||||
| golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM= |  | ||||||
| golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||||
| golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= | ||||||
| golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= | ||||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= | ||||||
| golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||||
| golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |  | ||||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | 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-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-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-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-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20191008105621-543471e840be/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-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-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-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-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-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | @ -211,28 +157,32 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w | ||||||
| golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | 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-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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4= |  | ||||||
| golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-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-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= | ||||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | golang.org/x/term v0.0.0-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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
| golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= |  | ||||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.3.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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | 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 h1:XDLXhn7ryprJVo+Lpkiib6CIuXE2031GDwtfEm7vLjI= | ||||||
| golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg= | golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg= | ||||||
| golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto= | golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto= | ||||||
| golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ= | golang.zx2c4.com/wireguard/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | 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/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 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= | ||||||
| gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= | 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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | 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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -1,29 +1,37 @@ | ||||||
| package handler | package handler | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"crypto/subtle" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/fs" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  | 	"regexp" | ||||||
| 	"sort" | 	"sort" | ||||||
|  | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	rice "github.com/GeertJohan/go.rice" |  | ||||||
| 	"github.com/gorilla/sessions" | 	"github.com/gorilla/sessions" | ||||||
| 	"github.com/labstack/echo-contrib/session" | 	"github.com/labstack/echo-contrib/session" | ||||||
| 	"github.com/labstack/echo/v4" | 	"github.com/labstack/echo/v4" | ||||||
| 	"github.com/labstack/gommon/log" | 	"github.com/labstack/gommon/log" | ||||||
| 	"github.com/rs/xid" | 	"github.com/rs/xid" | ||||||
|  | 	"github.com/skip2/go-qrcode" | ||||||
| 	"golang.zx2c4.com/wireguard/wgctrl" | 	"golang.zx2c4.com/wireguard/wgctrl" | ||||||
| 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes" | 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes" | ||||||
| 
 | 
 | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/emailer" | 	"github.com/ngoduykhanh/wireguard-ui/emailer" | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/model" | 	"github.com/ngoduykhanh/wireguard-ui/model" | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/store" | 	"github.com/ngoduykhanh/wireguard-ui/store" | ||||||
|  | 	"github.com/ngoduykhanh/wireguard-ui/telegram" | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/util" | 	"github.com/ngoduykhanh/wireguard-ui/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | var usernameRegexp = regexp.MustCompile("^\\w[\\w\\-.]*$") | ||||||
|  | 
 | ||||||
| // Health check handler
 | // Health check handler
 | ||||||
| func Health() echo.HandlerFunc { | func Health() echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
|  | @ -31,6 +39,15 @@ func Health() echo.HandlerFunc { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func Favicon() echo.HandlerFunc { | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		if favicon, ok := os.LookupEnv(util.FaviconFilePathEnvVar); ok { | ||||||
|  | 			return c.File(favicon) | ||||||
|  | 		} | ||||||
|  | 		return c.Redirect(http.StatusFound, util.BasePath+"/static/custom/img/favicon.ico") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // LoginPage handler
 | // LoginPage handler
 | ||||||
| func LoginPage() echo.HandlerFunc { | func LoginPage() echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
|  | @ -41,34 +58,76 @@ func LoginPage() echo.HandlerFunc { | ||||||
| // Login for signing in handler
 | // Login for signing in handler
 | ||||||
| func Login(db store.IStore) echo.HandlerFunc { | func Login(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 		user := new(model.User) | 		data := make(map[string]interface{}) | ||||||
| 		c.Bind(user) | 		err := json.NewDecoder(c.Request().Body).Decode(&data) | ||||||
| 
 | 
 | ||||||
| 		dbuser, err := db.GetUser() |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot query user from DB"}) | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if user.Username == dbuser.Username && user.Password == dbuser.Password { | 		username := data["username"].(string) | ||||||
| 			// TODO: refresh the token
 | 		password := data["password"].(string) | ||||||
|  | 		rememberMe := data["rememberMe"].(bool) | ||||||
|  | 
 | ||||||
|  | 		if !usernameRegexp.MatchString(username) { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		dbuser, err := db.GetUserByName(username) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Infof("Cannot query user %s from DB", username) | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Invalid credentials"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		userCorrect := subtle.ConstantTimeCompare([]byte(username), []byte(dbuser.Username)) == 1 | ||||||
|  | 
 | ||||||
|  | 		var passwordCorrect bool | ||||||
|  | 		if dbuser.PasswordHash != "" { | ||||||
|  | 			match, err := util.VerifyHash(dbuser.PasswordHash, password) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify password"}) | ||||||
|  | 			} | ||||||
|  | 			passwordCorrect = match | ||||||
|  | 		} else { | ||||||
|  | 			passwordCorrect = subtle.ConstantTimeCompare([]byte(password), []byte(dbuser.Password)) == 1 | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if userCorrect && passwordCorrect { | ||||||
|  | 			ageMax := 0 | ||||||
|  | 			if rememberMe { | ||||||
|  | 				ageMax = 86400 * 7 | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			cookiePath := util.GetCookiePath() | ||||||
|  | 
 | ||||||
| 			sess, _ := session.Get("session", c) | 			sess, _ := session.Get("session", c) | ||||||
| 			sess.Options = &sessions.Options{ | 			sess.Options = &sessions.Options{ | ||||||
| 				Path:     "/", | 				Path:     cookiePath, | ||||||
| 				MaxAge:   86400, | 				MaxAge:   ageMax, | ||||||
| 				HttpOnly: true, | 				HttpOnly: true, | ||||||
|  | 				SameSite: http.SameSiteLaxMode, | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// set session_token
 | 			// set session_token
 | ||||||
| 			tokenUID := xid.New().String() | 			tokenUID := xid.New().String() | ||||||
| 			sess.Values["username"] = user.Username | 			now := time.Now().UTC().Unix() | ||||||
|  | 			sess.Values["username"] = dbuser.Username | ||||||
|  | 			sess.Values["user_hash"] = util.GetDBUserCRC32(dbuser) | ||||||
|  | 			sess.Values["admin"] = dbuser.Admin | ||||||
| 			sess.Values["session_token"] = tokenUID | 			sess.Values["session_token"] = tokenUID | ||||||
|  | 			sess.Values["max_age"] = ageMax | ||||||
|  | 			sess.Values["created_at"] = now | ||||||
|  | 			sess.Values["updated_at"] = now | ||||||
| 			sess.Save(c.Request(), c.Response()) | 			sess.Save(c.Request(), c.Response()) | ||||||
| 
 | 
 | ||||||
| 			// set session_token in cookie
 | 			// set session_token in cookie
 | ||||||
| 			cookie := new(http.Cookie) | 			cookie := new(http.Cookie) | ||||||
| 			cookie.Name = "session_token" | 			cookie.Name = "session_token" | ||||||
|  | 			cookie.Path = cookiePath | ||||||
| 			cookie.Value = tokenUID | 			cookie.Value = tokenUID | ||||||
| 			cookie.Expires = time.Now().Add(24 * time.Hour) | 			cookie.MaxAge = ageMax | ||||||
|  | 			cookie.HttpOnly = true | ||||||
|  | 			cookie.SameSite = http.SameSiteLaxMode | ||||||
| 			c.SetCookie(cookie) | 			c.SetCookie(cookie) | ||||||
| 
 | 
 | ||||||
| 			return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Logged in successfully"}) | 			return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Logged in successfully"}) | ||||||
|  | @ -78,18 +137,221 @@ func Login(db store.IStore) echo.HandlerFunc { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // GetUsers handler return a JSON list of all users
 | ||||||
|  | func GetUsers(db store.IStore) echo.HandlerFunc { | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		usersList, err := db.GetUsers() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | ||||||
|  | 				false, fmt.Sprintf("Cannot get user list: %v", err), | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return c.JSON(http.StatusOK, usersList) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetUser handler returns a JSON object of single user
 | ||||||
|  | func GetUser(db store.IStore) echo.HandlerFunc { | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		username := c.Param("username") | ||||||
|  | 
 | ||||||
|  | 		if !usernameRegexp.MatchString(username) { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if !isAdmin(c) && (username != currentUser(c)) { | ||||||
|  | 			return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		userData, err := db.GetUserByName(username) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "User not found"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return c.JSON(http.StatusOK, userData) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Logout to log a user out
 | // Logout to log a user out
 | ||||||
| func Logout() echo.HandlerFunc { | func Logout() echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 		clearSession(c) | 		clearSession(c) | ||||||
| 		return c.Redirect(http.StatusTemporaryRedirect, "/login") | 		return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // LoadProfile to load user information
 | ||||||
|  | func LoadProfile() echo.HandlerFunc { | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		return c.Render(http.StatusOK, "profile.html", map[string]interface{}{ | ||||||
|  | 			"baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UsersSettings handler
 | ||||||
|  | func UsersSettings() echo.HandlerFunc { | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		return c.Render(http.StatusOK, "users_settings.html", map[string]interface{}{ | ||||||
|  | 			"baseData": model.BaseData{Active: "users-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UpdateUser to update user information
 | ||||||
|  | func UpdateUser(db store.IStore) echo.HandlerFunc { | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		data := make(map[string]interface{}) | ||||||
|  | 		err := json.NewDecoder(c.Request().Body).Decode(&data) | ||||||
|  | 
 | ||||||
|  | 		if err != nil { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		username := data["username"].(string) | ||||||
|  | 		password := data["password"].(string) | ||||||
|  | 		previousUsername := data["previous_username"].(string) | ||||||
|  | 		admin := data["admin"].(bool) | ||||||
|  | 
 | ||||||
|  | 		if !isAdmin(c) && (previousUsername != currentUser(c)) { | ||||||
|  | 			return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if !isAdmin(c) { | ||||||
|  | 			admin = false | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if !usernameRegexp.MatchString(previousUsername) { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		user, err := db.GetUserByName(previousUsername) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if username == "" || !usernameRegexp.MatchString(username) { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) | ||||||
|  | 		} else { | ||||||
|  | 			user.Username = username | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if username != previousUsername { | ||||||
|  | 			_, err := db.GetUserByName(username) | ||||||
|  | 			if err == nil { | ||||||
|  | 				return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "This username is taken"}) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if password != "" { | ||||||
|  | 			hash, err := util.HashPassword(password) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) | ||||||
|  | 			} | ||||||
|  | 			user.PasswordHash = hash | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if previousUsername != currentUser(c) { | ||||||
|  | 			user.Admin = admin | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if err := db.DeleteUser(previousUsername); err != nil { | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) | ||||||
|  | 		} | ||||||
|  | 		if err := db.SaveUser(user); err != nil { | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) | ||||||
|  | 		} | ||||||
|  | 		log.Infof("Updated user information successfully") | ||||||
|  | 
 | ||||||
|  | 		if previousUsername == currentUser(c) { | ||||||
|  | 			setUser(c, user.Username, user.Admin, util.GetDBUserCRC32(user)) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated user information successfully"}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CreateUser to create new user
 | ||||||
|  | func CreateUser(db store.IStore) echo.HandlerFunc { | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		data := make(map[string]interface{}) | ||||||
|  | 		err := json.NewDecoder(c.Request().Body).Decode(&data) | ||||||
|  | 
 | ||||||
|  | 		if err != nil { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var user model.User | ||||||
|  | 		username := data["username"].(string) | ||||||
|  | 		password := data["password"].(string) | ||||||
|  | 		admin := data["admin"].(bool) | ||||||
|  | 
 | ||||||
|  | 		if username == "" || !usernameRegexp.MatchString(username) { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) | ||||||
|  | 		} else { | ||||||
|  | 			user.Username = username | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		{ | ||||||
|  | 			_, err := db.GetUserByName(username) | ||||||
|  | 			if err == nil { | ||||||
|  | 				return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "This username is taken"}) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		hash, err := util.HashPassword(password) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) | ||||||
|  | 		} | ||||||
|  | 		user.PasswordHash = hash | ||||||
|  | 
 | ||||||
|  | 		user.Admin = admin | ||||||
|  | 
 | ||||||
|  | 		if err := db.SaveUser(user); err != nil { | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) | ||||||
|  | 		} | ||||||
|  | 		log.Infof("Created user successfully") | ||||||
|  | 
 | ||||||
|  | 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Created user successfully"}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RemoveUser handler
 | ||||||
|  | func RemoveUser(db store.IStore) echo.HandlerFunc { | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		data := make(map[string]interface{}) | ||||||
|  | 		err := json.NewDecoder(c.Request().Body).Decode(&data) | ||||||
|  | 
 | ||||||
|  | 		if err != nil { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		username := data["username"].(string) | ||||||
|  | 
 | ||||||
|  | 		if !usernameRegexp.MatchString(username) { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if username == currentUser(c) { | ||||||
|  | 			return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "User cannot delete itself"}) | ||||||
|  | 		} | ||||||
|  | 		// delete user from database
 | ||||||
|  | 
 | ||||||
|  | 		if err := db.DeleteUser(username); err != nil { | ||||||
|  | 			log.Error("Cannot delete user: ", err) | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot delete user from database"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		log.Infof("Removed user: %s", username) | ||||||
|  | 
 | ||||||
|  | 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "User removed"}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // WireGuardClients handler
 | // WireGuardClients handler
 | ||||||
| func WireGuardClients(db store.IStore) echo.HandlerFunc { | func WireGuardClients(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		clientDataList, err := db.GetClients(true) | 		clientDataList, err := db.GetClients(true) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | ||||||
|  | @ -98,16 +360,15 @@ func WireGuardClients(db store.IStore) echo.HandlerFunc { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return c.Render(http.StatusOK, "clients.html", map[string]interface{}{ | 		return c.Render(http.StatusOK, "clients.html", map[string]interface{}{ | ||||||
| 			"baseData":       model.BaseData{Active: "", CurrentUser: currentUser(c)}, | 			"baseData":       model.BaseData{Active: "", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 			"clientDataList": clientDataList, | 			"clientDataList": clientDataList, | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetClients handler return a list of Wireguard client data
 | // GetClients handler return a JSON list of Wireguard client data
 | ||||||
| func GetClients(db store.IStore) echo.HandlerFunc { | func GetClients(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		clientDataList, err := db.GetClients(true) | 		clientDataList, err := db.GetClients(true) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | ||||||
|  | @ -115,31 +376,52 @@ func GetClients(db store.IStore) echo.HandlerFunc { | ||||||
| 			}) | 			}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		for i, clientData := range clientDataList { | ||||||
|  | 			clientDataList[i] = util.FillClientSubnetRange(clientData) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		return c.JSON(http.StatusOK, clientDataList) | 		return c.JSON(http.StatusOK, clientDataList) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetClient handler return a of Wireguard client data
 | // GetClient handler returns a JSON object of Wireguard client data
 | ||||||
| func GetClient(db store.IStore) echo.HandlerFunc { | func GetClient(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		clientID := c.Param("id") | 		clientID := c.Param("id") | ||||||
| 		clientData, err := db.GetClientByID(clientID, true) | 
 | ||||||
|  | 		if _, err := xid.FromString(clientID); err != nil { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		qrCodeSettings := model.QRCodeSettings{ | ||||||
|  | 			Enabled:    true, | ||||||
|  | 			IncludeDNS: true, | ||||||
|  | 			IncludeMTU: true, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		clientData, err := db.GetClientByID(clientID, qrCodeSettings) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) | 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return c.JSON(http.StatusOK, clientData) | 		return c.JSON(http.StatusOK, util.FillClientSubnetRange(clientData)) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewClient handler
 | // NewClient handler
 | ||||||
| func NewClient(db store.IStore) echo.HandlerFunc { | func NewClient(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		var client model.Client | 		var client model.Client | ||||||
| 		c.Bind(&client) | 		c.Bind(&client) | ||||||
| 
 | 
 | ||||||
|  | 		// Validate Telegram userid if provided
 | ||||||
|  | 		if client.TgUserid != "" { | ||||||
|  | 			idNum, err := strconv.ParseInt(client.TgUserid, 10, 64) | ||||||
|  | 			if err != nil || idNum == 0 { | ||||||
|  | 				return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Telegram userid must be a non-zero number"}) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		// read server information
 | 		// read server information
 | ||||||
| 		server, err := db.GetServer() | 		server, err := db.GetServer() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -197,7 +479,6 @@ func NewClient(db store.IStore) echo.HandlerFunc { | ||||||
| 					return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Duplicate Public Key"}) | 					return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Duplicate Public Key"}) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if client.PresharedKey == "" { | 		if client.PresharedKey == "" { | ||||||
|  | @ -209,6 +490,9 @@ func NewClient(db store.IStore) echo.HandlerFunc { | ||||||
| 				}) | 				}) | ||||||
| 			} | 			} | ||||||
| 			client.PresharedKey = presharedKey.String() | 			client.PresharedKey = presharedKey.String() | ||||||
|  | 		} else if client.PresharedKey == "-" { | ||||||
|  | 			client.PresharedKey = "" | ||||||
|  | 			log.Infof("skipped PresharedKey generation for user: %v", client.Name) | ||||||
| 		} else { | 		} else { | ||||||
| 			_, err := wgtypes.ParseKey(client.PresharedKey) | 			_, err := wgtypes.ParseKey(client.PresharedKey) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
|  | @ -231,7 +515,7 @@ func NewClient(db store.IStore) echo.HandlerFunc { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // EmailClient handler to sent the configuration via email
 | // EmailClient handler to send the configuration via email
 | ||||||
| func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailContent string) echo.HandlerFunc { | func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailContent string) echo.HandlerFunc { | ||||||
| 	type clientIdEmailPayload struct { | 	type clientIdEmailPayload struct { | ||||||
| 		ID    string `json:"id"` | 		ID    string `json:"id"` | ||||||
|  | @ -243,7 +527,16 @@ func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailCon | ||||||
| 		c.Bind(&payload) | 		c.Bind(&payload) | ||||||
| 		// TODO validate email
 | 		// TODO validate email
 | ||||||
| 
 | 
 | ||||||
| 		clientData, err := db.GetClientByID(payload.ID, true) | 		if _, err := xid.FromString(payload.ID); err != nil { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		qrCodeSettings := model.QRCodeSettings{ | ||||||
|  | 			Enabled:    true, | ||||||
|  | 			IncludeDNS: true, | ||||||
|  | 			IncludeMTU: true, | ||||||
|  | 		} | ||||||
|  | 		clientData, err := db.GetClientByID(payload.ID, qrCodeSettings) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Errorf("Cannot generate client id %s config file for downloading: %v", payload.ID, err) | 			log.Errorf("Cannot generate client id %s config file for downloading: %v", payload.ID, err) | ||||||
| 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) | 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) | ||||||
|  | @ -254,17 +547,17 @@ func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailCon | ||||||
| 		globalSettings, _ := db.GetGlobalSettings() | 		globalSettings, _ := db.GetGlobalSettings() | ||||||
| 		config := util.BuildClientConfig(*clientData.Client, server, globalSettings) | 		config := util.BuildClientConfig(*clientData.Client, server, globalSettings) | ||||||
| 
 | 
 | ||||||
| 		cfg_att := emailer.Attachment{"wg0.conf", []byte(config)} | 		cfgAtt := emailer.Attachment{Name: "wg0.conf", Data: []byte(config)} | ||||||
| 		var attachments []emailer.Attachment | 		var attachments []emailer.Attachment | ||||||
| 		if clientData.Client.PrivateKey != "" { | 		if clientData.Client.PrivateKey != "" { | ||||||
| 			qrdata, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(clientData.QRCode, "data:image/png;base64,")) | 			qrdata, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(clientData.QRCode, "data:image/png;base64,")) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "decoding: " + err.Error()}) | 				return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "decoding: " + err.Error()}) | ||||||
| 			} | 			} | ||||||
| 			qr_att := emailer.Attachment{"wg.png", qrdata} | 			qrAtt := emailer.Attachment{Name: "wg.png", Data: qrdata} | ||||||
| 			attachments = []emailer.Attachment{cfg_att, qr_att} | 			attachments = []emailer.Attachment{cfgAtt, qrAtt} | ||||||
| 		} else { | 		} else { | ||||||
| 			attachments = []emailer.Attachment{cfg_att} | 			attachments = []emailer.Attachment{cfgAtt} | ||||||
| 		} | 		} | ||||||
| 		err = mailer.Send( | 		err = mailer.Send( | ||||||
| 			clientData.Client.Name, | 			clientData.Client.Name, | ||||||
|  | @ -282,19 +575,75 @@ func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailCon | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // SendTelegramClient handler to send the configuration via Telegram
 | ||||||
|  | func SendTelegramClient(db store.IStore) echo.HandlerFunc { | ||||||
|  | 	type clientIdUseridPayload struct { | ||||||
|  | 		ID     string `json:"id"` | ||||||
|  | 		Userid string `json:"userid"` | ||||||
|  | 	} | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		var payload clientIdUseridPayload | ||||||
|  | 		c.Bind(&payload) | ||||||
|  | 
 | ||||||
|  | 		clientData, err := db.GetClientByID(payload.ID, model.QRCodeSettings{Enabled: false}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Errorf("Cannot generate client id %s config file for downloading: %v", payload.ID, err) | ||||||
|  | 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// build config
 | ||||||
|  | 		server, _ := db.GetServer() | ||||||
|  | 		globalSettings, _ := db.GetGlobalSettings() | ||||||
|  | 		config := util.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 c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "qr gen: " + err.Error()}) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		userid, err := strconv.ParseInt(clientData.Client.TgUserid, 10, 64) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "userid: " + err.Error()}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		err = telegram.SendConfig(userid, clientData.Client.Name, configData, qrData, false) | ||||||
|  | 
 | ||||||
|  | 		if err != nil { | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Telegram message sent successfully"}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // UpdateClient handler to update client information
 | // UpdateClient handler to update client information
 | ||||||
| func UpdateClient(db store.IStore) echo.HandlerFunc { | func UpdateClient(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		var _client model.Client | 		var _client model.Client | ||||||
| 		c.Bind(&_client) | 		c.Bind(&_client) | ||||||
| 
 | 
 | ||||||
|  | 		if _, err := xid.FromString(_client.ID); err != nil { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		// validate client existence
 | 		// validate client existence
 | ||||||
| 		clientData, err := db.GetClientByID(_client.ID, false) | 		clientData, err := db.GetClientByID(_client.ID, model.QRCodeSettings{Enabled: false}) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) | 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		// Validate Telegram userid if provided
 | ||||||
|  | 		if _client.TgUserid != "" { | ||||||
|  | 			idNum, err := strconv.ParseInt(_client.TgUserid, 10, 64) | ||||||
|  | 			if err != nil || idNum == 0 { | ||||||
|  | 				return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Telegram userid must be a non-zero number"}) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		server, err := db.GetServer() | 		server, err := db.GetServer() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{ | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{ | ||||||
|  | @ -320,15 +669,58 @@ func UpdateClient(db store.IStore) echo.HandlerFunc { | ||||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Extra Allowed IPs must be in CIDR format"}) | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Extra Allowed IPs must be in CIDR format"}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		// update Wireguard Client PublicKey
 | ||||||
|  | 		if client.PublicKey != _client.PublicKey && _client.PublicKey != "" { | ||||||
|  | 			_, err := wgtypes.ParseKey(_client.PublicKey) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("Cannot verify provided Wireguard public key: ", err) | ||||||
|  | 				return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify provided Wireguard public key"}) | ||||||
|  | 			} | ||||||
|  | 			// check for duplicates
 | ||||||
|  | 			clients, err := db.GetClients(false) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("Cannot get client list for duplicate public key check") | ||||||
|  | 				return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client list for duplicate public key check"}) | ||||||
|  | 			} | ||||||
|  | 			for _, other := range clients { | ||||||
|  | 				if other.Client.PublicKey == _client.PublicKey { | ||||||
|  | 					log.Error("Duplicate Public Key") | ||||||
|  | 					return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Duplicate Public Key"}) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// When replacing any PublicKey, discard any locally stored Wireguard Client PrivateKey
 | ||||||
|  | 			// Client PubKey no longer corresponds to locally stored PrivKey.
 | ||||||
|  | 			// QR code (needs PrivateKey) for this client is no longer possible now.
 | ||||||
|  | 
 | ||||||
|  | 			if client.PrivateKey != "" { | ||||||
|  | 				client.PrivateKey = "" | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// update Wireguard Client PresharedKey
 | ||||||
|  | 		if client.PresharedKey != _client.PresharedKey && _client.PresharedKey != "" { | ||||||
|  | 			_, err := wgtypes.ParseKey(_client.PresharedKey) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("Cannot verify provided Wireguard preshared key: ", err) | ||||||
|  | 				return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify provided Wireguard preshared key"}) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		// map new data
 | 		// map new data
 | ||||||
| 		client.Name = _client.Name | 		client.Name = _client.Name | ||||||
| 		client.Email = _client.Email | 		client.Email = _client.Email | ||||||
|  | 		client.TgUserid = _client.TgUserid | ||||||
| 		client.Enabled = _client.Enabled | 		client.Enabled = _client.Enabled | ||||||
| 		client.UseServerDNS = _client.UseServerDNS | 		client.UseServerDNS = _client.UseServerDNS | ||||||
| 		client.AllocatedIPs = _client.AllocatedIPs | 		client.AllocatedIPs = _client.AllocatedIPs | ||||||
| 		client.AllowedIPs = _client.AllowedIPs | 		client.AllowedIPs = _client.AllowedIPs | ||||||
| 		client.ExtraAllowedIPs = _client.ExtraAllowedIPs | 		client.ExtraAllowedIPs = _client.ExtraAllowedIPs | ||||||
|  | 		client.Endpoint = _client.Endpoint | ||||||
|  | 		client.PublicKey = _client.PublicKey | ||||||
|  | 		client.PresharedKey = _client.PresharedKey | ||||||
| 		client.UpdatedAt = time.Now().UTC() | 		client.UpdatedAt = time.Now().UTC() | ||||||
|  | 		client.AdditionalNotes = strings.ReplaceAll(strings.Trim(_client.AdditionalNotes, "\r\n"), "\r\n", "\n") | ||||||
| 
 | 
 | ||||||
| 		// write to the database
 | 		// write to the database
 | ||||||
| 		if err := db.SaveClient(client); err != nil { | 		if err := db.SaveClient(client); err != nil { | ||||||
|  | @ -343,7 +735,6 @@ func UpdateClient(db store.IStore) echo.HandlerFunc { | ||||||
| // SetClientStatus handler to enable / disable a client
 | // SetClientStatus handler to enable / disable a client
 | ||||||
| func SetClientStatus(db store.IStore) echo.HandlerFunc { | func SetClientStatus(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		data := make(map[string]interface{}) | 		data := make(map[string]interface{}) | ||||||
| 		err := json.NewDecoder(c.Request().Body).Decode(&data) | 		err := json.NewDecoder(c.Request().Body).Decode(&data) | ||||||
| 
 | 
 | ||||||
|  | @ -354,12 +745,16 @@ func SetClientStatus(db store.IStore) echo.HandlerFunc { | ||||||
| 		clientID := data["id"].(string) | 		clientID := data["id"].(string) | ||||||
| 		status := data["status"].(bool) | 		status := data["status"].(bool) | ||||||
| 
 | 
 | ||||||
| 		clientdata, err := db.GetClientByID(clientID, false) | 		if _, err := xid.FromString(clientID); err != nil { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		clientData, err := db.GetClientByID(clientID, model.QRCodeSettings{Enabled: false}) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) | 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		client := *clientdata.Client | 		client := *clientData.Client | ||||||
| 
 | 
 | ||||||
| 		client.Enabled = status | 		client.Enabled = status | ||||||
| 		if err := db.SaveClient(client); err != nil { | 		if err := db.SaveClient(client); err != nil { | ||||||
|  | @ -379,7 +774,11 @@ func DownloadClient(db store.IStore) echo.HandlerFunc { | ||||||
| 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Missing clientid parameter"}) | 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Missing clientid parameter"}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		clientData, err := db.GetClientByID(clientID, false) | 		if _, err := xid.FromString(clientID); err != nil { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		clientData, err := db.GetClientByID(clientID, model.QRCodeSettings{Enabled: false}) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Errorf("Cannot generate client id %s config file for downloading: %v", clientID, err) | 			log.Errorf("Cannot generate client id %s config file for downloading: %v", clientID, err) | ||||||
| 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) | 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) | ||||||
|  | @ -401,17 +800,20 @@ func DownloadClient(db store.IStore) echo.HandlerFunc { | ||||||
| 
 | 
 | ||||||
| 		// set response header for downloading
 | 		// set response header for downloading
 | ||||||
| 		c.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%s.conf", clientData.Client.Name)) | 		c.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%s.conf", clientData.Client.Name)) | ||||||
| 		return c.Stream(http.StatusOK, "text/plain", reader) | 		return c.Stream(http.StatusOK, "text/conf", reader) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RemoveClient handler
 | // RemoveClient handler
 | ||||||
| func RemoveClient(db store.IStore) echo.HandlerFunc { | func RemoveClient(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		client := new(model.Client) | 		client := new(model.Client) | ||||||
| 		c.Bind(client) | 		c.Bind(client) | ||||||
| 
 | 
 | ||||||
|  | 		if _, err := xid.FromString(client.ID); err != nil { | ||||||
|  | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		// delete client from database
 | 		// delete client from database
 | ||||||
| 
 | 
 | ||||||
| 		if err := db.DeleteClient(client.ID); err != nil { | 		if err := db.DeleteClient(client.ID); err != nil { | ||||||
|  | @ -427,14 +829,13 @@ func RemoveClient(db store.IStore) echo.HandlerFunc { | ||||||
| // WireGuardServer handler
 | // WireGuardServer handler
 | ||||||
| func WireGuardServer(db store.IStore) echo.HandlerFunc { | func WireGuardServer(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		server, err := db.GetServer() | 		server, err := db.GetServer() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error("Cannot get server config: ", err) | 			log.Error("Cannot get server config: ", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return c.Render(http.StatusOK, "server.html", map[string]interface{}{ | 		return c.Render(http.StatusOK, "server.html", map[string]interface{}{ | ||||||
| 			"baseData":        model.BaseData{Active: "wg-server", CurrentUser: currentUser(c)}, | 			"baseData":        model.BaseData{Active: "wg-server", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 			"serverInterface": server.Interface, | 			"serverInterface": server.Interface, | ||||||
| 			"serverKeyPair":   server.KeyPair, | 			"serverKeyPair":   server.KeyPair, | ||||||
| 		}) | 		}) | ||||||
|  | @ -444,7 +845,6 @@ func WireGuardServer(db store.IStore) echo.HandlerFunc { | ||||||
| // WireGuardServerInterfaces handler
 | // WireGuardServerInterfaces handler
 | ||||||
| func WireGuardServerInterfaces(db store.IStore) echo.HandlerFunc { | func WireGuardServerInterfaces(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		var serverInterface model.ServerInterface | 		var serverInterface model.ServerInterface | ||||||
| 		c.Bind(&serverInterface) | 		c.Bind(&serverInterface) | ||||||
| 
 | 
 | ||||||
|  | @ -470,7 +870,6 @@ func WireGuardServerInterfaces(db store.IStore) echo.HandlerFunc { | ||||||
| // WireGuardServerKeyPair handler to generate private and public keys
 | // WireGuardServerKeyPair handler to generate private and public keys
 | ||||||
| func WireGuardServerKeyPair(db store.IStore) echo.HandlerFunc { | func WireGuardServerKeyPair(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		// gen Wireguard key pair
 | 		// gen Wireguard key pair
 | ||||||
| 		key, err := wgtypes.GeneratePrivateKey() | 		key, err := wgtypes.GeneratePrivateKey() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -495,14 +894,13 @@ func WireGuardServerKeyPair(db store.IStore) echo.HandlerFunc { | ||||||
| // GlobalSettings handler
 | // GlobalSettings handler
 | ||||||
| func GlobalSettings(db store.IStore) echo.HandlerFunc { | func GlobalSettings(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		globalSettings, err := db.GetGlobalSettings() | 		globalSettings, err := db.GetGlobalSettings() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error("Cannot get global settings: ", err) | 			log.Error("Cannot get global settings: ", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{ | 		return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{ | ||||||
| 			"baseData":       model.BaseData{Active: "global-settings", CurrentUser: currentUser(c)}, | 			"baseData":       model.BaseData{Active: "global-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 			"globalSettings": globalSettings, | 			"globalSettings": globalSettings, | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  | @ -519,6 +917,8 @@ func Status(db store.IStore) echo.HandlerFunc { | ||||||
| 		LastHandshakeTime time.Time | 		LastHandshakeTime time.Time | ||||||
| 		LastHandshakeRel  time.Duration | 		LastHandshakeRel  time.Duration | ||||||
| 		Connected         bool | 		Connected         bool | ||||||
|  | 		AllocatedIP       string | ||||||
|  | 		Endpoint          string | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	type DeviceVM struct { | 	type DeviceVM struct { | ||||||
|  | @ -526,20 +926,19 @@ func Status(db store.IStore) echo.HandlerFunc { | ||||||
| 		Peers []PeerVM | 		Peers []PeerVM | ||||||
| 	} | 	} | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 | 		wgClient, err := wgctrl.New() | ||||||
| 		wgclient, err := wgctrl.New() |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | 			return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | ||||||
| 				"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, | 				"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 				"error":    err.Error(), | 				"error":    err.Error(), | ||||||
| 				"devices":  nil, | 				"devices":  nil, | ||||||
| 			}) | 			}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		devices, err := wgclient.Devices() | 		devices, err := wgClient.Devices() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | 			return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | ||||||
| 				"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, | 				"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 				"error":    err.Error(), | 				"error":    err.Error(), | ||||||
| 				"devices":  nil, | 				"devices":  nil, | ||||||
| 			}) | 			}) | ||||||
|  | @ -551,7 +950,7 @@ func Status(db store.IStore) echo.HandlerFunc { | ||||||
| 			clients, err := db.GetClients(false) | 			clients, err := db.GetClients(false) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | 				return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | ||||||
| 					"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, | 					"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 					"error":    err.Error(), | 					"error":    err.Error(), | ||||||
| 					"devices":  nil, | 					"devices":  nil, | ||||||
| 				}) | 				}) | ||||||
|  | @ -566,15 +965,27 @@ func Status(db store.IStore) echo.HandlerFunc { | ||||||
| 			for i := range devices { | 			for i := range devices { | ||||||
| 				devVm := DeviceVM{Name: devices[i].Name} | 				devVm := DeviceVM{Name: devices[i].Name} | ||||||
| 				for j := range devices[i].Peers { | 				for j := range devices[i].Peers { | ||||||
|  | 					var allocatedIPs string | ||||||
|  | 					for _, ip := range devices[i].Peers[j].AllowedIPs { | ||||||
|  | 						if len(allocatedIPs) > 0 { | ||||||
|  | 							allocatedIPs += "</br>" | ||||||
|  | 						} | ||||||
|  | 						allocatedIPs += ip.String() | ||||||
|  | 					} | ||||||
| 					pVm := PeerVM{ | 					pVm := PeerVM{ | ||||||
| 						PublicKey:         devices[i].Peers[j].PublicKey.String(), | 						PublicKey:         devices[i].Peers[j].PublicKey.String(), | ||||||
| 						ReceivedBytes:     devices[i].Peers[j].ReceiveBytes, | 						ReceivedBytes:     devices[i].Peers[j].ReceiveBytes, | ||||||
| 						TransmitBytes:     devices[i].Peers[j].TransmitBytes, | 						TransmitBytes:     devices[i].Peers[j].TransmitBytes, | ||||||
| 						LastHandshakeTime: devices[i].Peers[j].LastHandshakeTime, | 						LastHandshakeTime: devices[i].Peers[j].LastHandshakeTime, | ||||||
| 						LastHandshakeRel:  time.Since(devices[i].Peers[j].LastHandshakeTime), | 						LastHandshakeRel:  time.Since(devices[i].Peers[j].LastHandshakeTime), | ||||||
|  | 						AllocatedIP:       allocatedIPs, | ||||||
| 					} | 					} | ||||||
| 					pVm.Connected = pVm.LastHandshakeRel.Minutes() < 3. | 					pVm.Connected = pVm.LastHandshakeRel.Minutes() < 3. | ||||||
| 
 | 
 | ||||||
|  | 					if isAdmin(c) { | ||||||
|  | 						pVm.Endpoint = devices[i].Peers[j].Endpoint.String() | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
| 					if _client, ok := m[pVm.PublicKey]; ok { | 					if _client, ok := m[pVm.PublicKey]; ok { | ||||||
| 						pVm.Name = _client.Name | 						pVm.Name = _client.Name | ||||||
| 						pVm.Email = _client.Email | 						pVm.Email = _client.Email | ||||||
|  | @ -588,7 +999,7 @@ func Status(db store.IStore) echo.HandlerFunc { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return c.Render(http.StatusOK, "status.html", map[string]interface{}{ | 		return c.Render(http.StatusOK, "status.html", map[string]interface{}{ | ||||||
| 			"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, | 			"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 			"devices":  devicesVm, | 			"devices":  devicesVm, | ||||||
| 			"error":    "", | 			"error":    "", | ||||||
| 		}) | 		}) | ||||||
|  | @ -598,7 +1009,6 @@ func Status(db store.IStore) echo.HandlerFunc { | ||||||
| // GlobalSettingSubmit handler to update the global settings
 | // GlobalSettingSubmit handler to update the global settings
 | ||||||
| func GlobalSettingSubmit(db store.IStore) echo.HandlerFunc { | func GlobalSettingSubmit(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		var globalSettings model.GlobalSetting | 		var globalSettings model.GlobalSetting | ||||||
| 		c.Bind(&globalSettings) | 		c.Bind(&globalSettings) | ||||||
| 
 | 
 | ||||||
|  | @ -624,7 +1034,6 @@ func GlobalSettingSubmit(db store.IStore) echo.HandlerFunc { | ||||||
| // MachineIPAddresses handler to get local interface ip addresses
 | // MachineIPAddresses handler to get local interface ip addresses
 | ||||||
| func MachineIPAddresses() echo.HandlerFunc { | func MachineIPAddresses() echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		// get private ip addresses
 | 		// get private ip addresses
 | ||||||
| 		interfaceList, err := util.GetInterfaceIPs() | 		interfaceList, err := util.GetInterfaceIPs() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -645,10 +1054,16 @@ func MachineIPAddresses() echo.HandlerFunc { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // GetOrderedSubnetRanges handler to get the ordered list of subnet ranges
 | ||||||
|  | func GetOrderedSubnetRanges() echo.HandlerFunc { | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		return c.JSON(http.StatusOK, util.SubnetRangesOrder) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // SuggestIPAllocation handler to get the list of ip address for client
 | // SuggestIPAllocation handler to get the list of ip address for client
 | ||||||
| func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { | func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		server, err := db.GetServer() | 		server, err := db.GetServer() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error("Cannot fetch server config from database: ", err) | 			log.Error("Cannot fetch server config from database: ", err) | ||||||
|  | @ -666,30 +1081,55 @@ func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { | ||||||
| 				false, "Cannot suggest ip allocation: failed to get list of allocated ip addresses", | 				false, "Cannot suggest ip allocation: failed to get list of allocated ip addresses", | ||||||
| 			}) | 			}) | ||||||
| 		} | 		} | ||||||
| 		for _, cidr := range server.Interface.Addresses { | 
 | ||||||
| 			ip, err := util.GetAvailableIP(cidr, allocatedIPs) | 		sr := c.QueryParam("sr") | ||||||
|  | 		searchCIDRList := make([]string, 0) | ||||||
|  | 		found := false | ||||||
|  | 
 | ||||||
|  | 		// Use subnet range or default to interface addresses
 | ||||||
|  | 		if util.SubnetRanges[sr] != nil { | ||||||
|  | 			for _, cidr := range util.SubnetRanges[sr] { | ||||||
|  | 				searchCIDRList = append(searchCIDRList, cidr.String()) | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			searchCIDRList = append(searchCIDRList, server.Interface.Addresses...) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Save only unique IPs
 | ||||||
|  | 		ipSet := make(map[string]struct{}) | ||||||
|  | 
 | ||||||
|  | 		for _, cidr := range searchCIDRList { | ||||||
|  | 			ip, err := util.GetAvailableIP(cidr, allocatedIPs, server.Interface.Addresses) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Error("Failed to get available ip from a CIDR: ", err) | 				log.Error("Failed to get available ip from a CIDR: ", err) | ||||||
| 				return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | 				continue | ||||||
| 					false, |  | ||||||
| 					fmt.Sprintf("Cannot suggest ip allocation: failed to get available ip from network %s", cidr), |  | ||||||
| 				}) |  | ||||||
| 			} | 			} | ||||||
|  | 			found = true | ||||||
| 			if strings.Contains(ip, ":") { | 			if strings.Contains(ip, ":") { | ||||||
| 				suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/128", ip)) | 				ipSet[fmt.Sprintf("%s/128", ip)] = struct{}{} | ||||||
| 			} else { | 			} else { | ||||||
| 				suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/32", ip)) | 				ipSet[fmt.Sprintf("%s/32", ip)] = struct{}{} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		if !found { | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | ||||||
|  | 				false, | ||||||
|  | 				"Cannot suggest ip allocation: failed to get available ip. Try a different subnet or deallocate some ips.", | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for ip := range ipSet { | ||||||
|  | 			suggestedIPs = append(suggestedIPs, ip) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		return c.JSON(http.StatusOK, suggestedIPs) | 		return c.JSON(http.StatusOK, suggestedIPs) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ApplyServerConfig handler to write config file and restart Wireguard server
 | // ApplyServerConfig handler to write config file and restart Wireguard server
 | ||||||
| func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc { | func ApplyServerConfig(db store.IStore, tmplDir fs.FS) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 |  | ||||||
| 		server, err := db.GetServer() | 		server, err := db.GetServer() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error("Cannot get server config: ", err) | 			log.Error("Cannot get server config: ", err) | ||||||
|  | @ -702,6 +1142,12 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc { | ||||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"}) | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		users, err := db.GetUsers() | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("Cannot get users config: ", err) | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get users config"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		settings, err := db.GetGlobalSettings() | 		settings, err := db.GetGlobalSettings() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error("Cannot get global settings: ", err) | 			log.Error("Cannot get global settings: ", err) | ||||||
|  | @ -709,7 +1155,7 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// Write config file
 | 		// Write config file
 | ||||||
| 		err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings) | 		err = util.WriteWireGuardServerConfig(tmplDir, server, clients, users, settings) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error("Cannot apply server config: ", err) | 			log.Error("Cannot apply server config: ", err) | ||||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | ||||||
|  | @ -717,6 +1163,34 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc { | ||||||
| 			}) | 			}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		err = util.UpdateHashes(db) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("Cannot update hashes: ", err) | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | ||||||
|  | 				false, fmt.Sprintf("Cannot update hashes: %v", err), | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Applied server config successfully"}) | 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Applied server config successfully"}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // GetHashesChanges handler returns if database hashes have changed
 | ||||||
|  | func GetHashesChanges(db store.IStore) echo.HandlerFunc { | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		if util.HashesChanged(db) { | ||||||
|  | 			return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Hashes changed"}) | ||||||
|  | 		} else { | ||||||
|  | 			return c.JSON(http.StatusOK, jsonHTTPResponse{false, "Hashes not changed"}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AboutPage handler
 | ||||||
|  | func AboutPage() echo.HandlerFunc { | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		return c.Render(http.StatusOK, "about.html", map[string]interface{}{ | ||||||
|  | 			"baseData": model.BaseData{Active: "about", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -2,14 +2,15 @@ package handler | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net" | ||||||
|  | 	"net/http" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
| 	"github.com/labstack/echo/v4" | 	"github.com/labstack/echo/v4" | ||||||
| 	"github.com/labstack/gommon/log" | 	"github.com/labstack/gommon/log" | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/model" | 	"github.com/ngoduykhanh/wireguard-ui/model" | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/store" | 	"github.com/ngoduykhanh/wireguard-ui/store" | ||||||
| 	"github.com/sabhiram/go-wol/wol" | 	"github.com/sabhiram/go-wol/wol" | ||||||
| 	"net" |  | ||||||
| 	"net/http" |  | ||||||
| 	"time" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type WakeOnLanHostSavePayload struct { | type WakeOnLanHostSavePayload struct { | ||||||
|  | @ -37,7 +38,7 @@ func GetWakeOnLanHosts(db store.IStore) echo.HandlerFunc { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		err = c.Render(http.StatusOK, "wake_on_lan_hosts.html", map[string]interface{}{ | 		err = c.Render(http.StatusOK, "wake_on_lan_hosts.html", map[string]interface{}{ | ||||||
| 			"baseData": model.BaseData{Active: "wake_on_lan_hosts", CurrentUser: currentUser(c)}, | 			"baseData": model.BaseData{Active: "wake_on_lan_hosts", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 			"hosts":    hosts, | 			"hosts":    hosts, | ||||||
| 			"error":    "", | 			"error":    "", | ||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
|  | @ -3,7 +3,9 @@ package handler | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/gorilla/sessions" | ||||||
| 	"github.com/labstack/echo-contrib/session" | 	"github.com/labstack/echo-contrib/session" | ||||||
| 	"github.com/labstack/echo/v4" | 	"github.com/labstack/echo/v4" | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/util" | 	"github.com/ngoduykhanh/wireguard-ui/util" | ||||||
|  | @ -14,15 +16,33 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc { | ||||||
| 		if !isValidSession(c) { | 		if !isValidSession(c) { | ||||||
| 			nextURL := c.Request().URL | 			nextURL := c.Request().URL | ||||||
| 			if nextURL != nil && c.Request().Method == http.MethodGet { | 			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 { | 			} else { | ||||||
| 				return c.Redirect(http.StatusTemporaryRedirect, "/login") | 				return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login") | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		return next(c) | 		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 { | func isValidSession(c echo.Context) bool { | ||||||
| 	if util.DisableLogin { | 	if util.DisableLogin { | ||||||
| 		return true | 		return true | ||||||
|  | @ -32,9 +52,146 @@ func isValidSession(c echo.Context) bool { | ||||||
| 	if err != nil || sess.Values["session_token"] != cookie.Value { | 	if err != nil || sess.Values["session_token"] != cookie.Value { | ||||||
| 		return false | 		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 | 	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
 | // currentUser to get username of logged in user
 | ||||||
| func currentUser(c echo.Context) string { | func currentUser(c echo.Context) string { | ||||||
| 	if util.DisableLogin { | 	if util.DisableLogin { | ||||||
|  | @ -46,10 +203,47 @@ func currentUser(c echo.Context) string { | ||||||
| 	return username | 	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
 | // clearSession to remove current session
 | ||||||
| func clearSession(c echo.Context) { | func clearSession(c echo.Context) { | ||||||
| 	sess, _ := session.Get("session", c) | 	sess, _ := session.Get("session", c) | ||||||
| 	sess.Values["username"] = "" | 	sess.Values["username"] = "" | ||||||
|  | 	sess.Values["user_hash"] = 0 | ||||||
|  | 	sess.Values["admin"] = false | ||||||
| 	sess.Values["session_token"] = "" | 	sess.Values["session_token"] = "" | ||||||
|  | 	sess.Values["max_age"] = -1 | ||||||
|  | 	sess.Options.MaxAge = -1 | ||||||
| 	sess.Save(c.Request(), c.Response()) | 	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 $! | ||||||
							
								
								
									
										322
									
								
								main.go
								
								
								
								
							
							
						
						
									
										322
									
								
								main.go
								
								
								
								
							|  | @ -1,13 +1,23 @@ | ||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"crypto/sha512" | ||||||
|  | 	"embed" | ||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/labstack/echo/v4" | 	"io/fs" | ||||||
|  | 	"net" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | 	"syscall" | ||||||
| 	"time" | 	"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/emailer" | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/handler" | 	"github.com/ngoduykhanh/wireguard-ui/handler" | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/router" | 	"github.com/ngoduykhanh/wireguard-ui/router" | ||||||
|  | @ -22,18 +32,27 @@ var ( | ||||||
| 	gitRef     = "N/A" | 	gitRef     = "N/A" | ||||||
| 	buildTime  = fmt.Sprintf(time.Now().UTC().Format("01-02-2006 15:04:05")) | 	buildTime  = fmt.Sprintf(time.Now().UTC().Format("01-02-2006 15:04:05")) | ||||||
| 	// configuration variables
 | 	// configuration variables
 | ||||||
| 	flagDisableLogin   bool   = false | 	flagDisableLogin             = false | ||||||
| 	flagBindAddress    string = "0.0.0.0:5000" | 	flagBindAddress              = "0.0.0.0:5000" | ||||||
| 	flagSmtpHostname   string = "127.0.0.1" | 	flagSmtpHostname             = "127.0.0.1" | ||||||
| 	flagSmtpPort       int    = 25 | 	flagSmtpPort                 = 25 | ||||||
| 	flagSmtpUsername   string | 	flagSmtpUsername             string | ||||||
| 	flagSmtpPassword   string | 	flagSmtpPassword             string | ||||||
| 	flagSmtpAuthType   string = "None" | 	flagSmtpAuthType             = "NONE" | ||||||
| 	flagSmtpNoTLSCheck bool   = false | 	flagSmtpNoTLSCheck           = false | ||||||
| 	flagSendgridApiKey string | 	flagSmtpEncryption           = "STARTTLS" | ||||||
| 	flagEmailFrom      string | 	flagSmtpHelo                 = "localhost" | ||||||
| 	flagEmailFromName  string = "WireGuard UI" | 	flagSendgridApiKey           string | ||||||
| 	flagSessionSecret  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 ( | const ( | ||||||
|  | @ -45,21 +64,64 @@ 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
 | 	// 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.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(&flagBindAddress, "bind-address", util.LookupEnvOrString("BIND_ADDRESS", flagBindAddress), "Address:Port to which the app will be bound.") | ||||||
| 	flag.StringVar(&flagSmtpHostname, "smtp-hostname", util.LookupEnvOrString("SMTP_HOSTNAME", flagSmtpHostname), "SMTP Hostname") | 	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.IntVar(&flagSmtpPort, "smtp-port", util.LookupEnvOrInt("SMTP_PORT", flagSmtpPort), "SMTP Port") | ||||||
| 	flag.StringVar(&flagSmtpUsername, "smtp-username", util.LookupEnvOrString("SMTP_USERNAME", flagSmtpUsername), "SMTP Password") | 	flag.StringVar(&flagSmtpHelo, "smtp-helo", util.LookupEnvOrString("SMTP_HELO", flagSmtpHelo), "SMTP HELO Hostname") | ||||||
| 	flag.StringVar(&flagSmtpPassword, "smtp-password", util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword), "SMTP Password") | 	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.BoolVar(&flagSmtpNoTLSCheck, "smtp-no-tls-check", util.LookupEnvOrBool("SMTP_NO_TLS_CHECK", flagSmtpNoTLSCheck), "Disable TLS verification for SMTP. This is potentially dangerous.") | ||||||
| 	flag.StringVar(&flagSmtpAuthType, "smtp-auth-type", util.LookupEnvOrString("SMTP_AUTH_TYPE", flagSmtpAuthType), "SMTP Auth Type : Plain or None.") | 	flag.StringVar(&flagSmtpEncryption, "smtp-encryption", util.LookupEnvOrString("SMTP_ENCRYPTION", flagSmtpEncryption), "SMTP Encryption : NONE, SSL, SSLTLS, TLS or STARTTLS (by default)") | ||||||
| 	flag.StringVar(&flagSendgridApiKey, "sendgrid-api-key", util.LookupEnvOrString("SENDGRID_API_KEY", flagSendgridApiKey), "Your sendgrid api key.") | 	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(&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(&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() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	// update runtime config
 | 	// update runtime config
 | ||||||
|  | @ -67,29 +129,47 @@ func init() { | ||||||
| 	util.BindAddress = flagBindAddress | 	util.BindAddress = flagBindAddress | ||||||
| 	util.SmtpHostname = flagSmtpHostname | 	util.SmtpHostname = flagSmtpHostname | ||||||
| 	util.SmtpPort = flagSmtpPort | 	util.SmtpPort = flagSmtpPort | ||||||
|  | 	util.SmtpHelo = flagSmtpHelo | ||||||
| 	util.SmtpUsername = flagSmtpUsername | 	util.SmtpUsername = flagSmtpUsername | ||||||
| 	util.SmtpPassword = flagSmtpPassword | 	util.SmtpPassword = flagSmtpPassword | ||||||
| 	util.SmtpAuthType = flagSmtpAuthType | 	util.SmtpAuthType = flagSmtpAuthType | ||||||
| 	util.SmtpNoTLSCheck = flagSmtpNoTLSCheck | 	util.SmtpNoTLSCheck = flagSmtpNoTLSCheck | ||||||
|  | 	util.SmtpEncryption = flagSmtpEncryption | ||||||
| 	util.SendgridApiKey = flagSendgridApiKey | 	util.SendgridApiKey = flagSendgridApiKey | ||||||
| 	util.EmailFrom = flagEmailFrom | 	util.EmailFrom = flagEmailFrom | ||||||
| 	util.EmailFromName = flagEmailFromName | 	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
 | 	lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")) | ||||||
| 	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)
 |  | ||||||
| 
 | 
 | ||||||
|  | 	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() { | func main() { | ||||||
|  | @ -101,58 +181,160 @@ func main() { | ||||||
| 		panic(err) | 		panic(err) | ||||||
| 	} | 	} | ||||||
| 	// set app extra data
 | 	// set app extra data
 | ||||||
| 	extraData := make(map[string]string) | 	extraData := make(map[string]interface{}) | ||||||
| 	extraData["appVersion"] = appVersion | 	extraData["appVersion"] = appVersion | ||||||
|  | 	extraData["gitCommit"] = gitCommit | ||||||
|  | 	extraData["basePath"] = util.BasePath | ||||||
|  | 	extraData["loginDisabled"] = flagDisableLogin | ||||||
| 
 | 
 | ||||||
| 	// create rice box for embedded template
 | 	// strip the "templates/" prefix from the embedded directory so files can be read by their direct name (e.g.
 | ||||||
| 	tmplBox := rice.MustFindBox("templates") | 	// "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.
 | 	// create the wireguard config on start, if it doesn't exist
 | ||||||
| 	assetHandler := http.FileServer(rice.MustFindBox("assets").HTTPBox()) | 	initServerConfig(db, tmplDir) | ||||||
|  | 
 | ||||||
|  | 	// Check if subnet ranges are valid for the server configuration
 | ||||||
|  | 	// Remove any non-valid CIDRs
 | ||||||
|  | 	if err := util.ValidateAndFixSubnetRanges(db); err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Print valid ranges
 | ||||||
|  | 	if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO { | ||||||
|  | 		fmt.Println("Valid subnet ranges:", util.GetSubnetRangesString()) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	// register routes
 | 	// register routes
 | ||||||
| 	app := router.New(tmplBox, extraData, util.SessionSecret) | 	app := router.New(tmplDir, extraData, util.SessionSecret) | ||||||
| 
 | 
 | ||||||
| 	app.GET("/", handler.WireGuardClients(db), handler.ValidSession) | 	app.GET(util.BasePath, handler.WireGuardClients(db), handler.ValidSession, handler.RefreshSession) | ||||||
|  | 
 | ||||||
|  | 	// 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 { | 	if !util.DisableLogin { | ||||||
| 		app.GET("/login", handler.LoginPage()) | 		app.GET(util.BasePath+"/login", handler.LoginPage()) | ||||||
| 		app.POST("/login", handler.Login(db)) | 		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 | 	var sendmail emailer.Emailer | ||||||
| 	if util.SendgridApiKey != "" { | 	if util.SendgridApiKey != "" { | ||||||
| 		sendmail = emailer.NewSendgridApiMail(util.SendgridApiKey, util.EmailFromName, util.EmailFrom) | 		sendmail = emailer.NewSendgridApiMail(util.SendgridApiKey, util.EmailFromName, util.EmailFrom) | ||||||
| 	} else { | 	} else { | ||||||
| 		sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom) | 		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("/_health", handler.Health()) | 	app.GET(util.BasePath+"/test-hash", handler.GetHashesChanges(db), handler.ValidSession) | ||||||
| 	app.GET("/logout", handler.Logout(), handler.ValidSession) | 	app.GET(util.BasePath+"/about", handler.AboutPage()) | ||||||
| 	app.POST("/new-client", handler.NewClient(db), handler.ValidSession) | 	app.GET(util.BasePath+"/_health", handler.Health()) | ||||||
| 	app.POST("/update-client", handler.UpdateClient(db), handler.ValidSession) | 	app.GET(util.BasePath+"/favicon", handler.Favicon()) | ||||||
| 	app.POST("/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession) | 	app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.POST("/client/set-status", handler.SetClientStatus(db), handler.ValidSession) | 	app.POST(util.BasePath+"/update-client", handler.UpdateClient(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.POST("/remove-client", handler.RemoveClient(db), handler.ValidSession) | 	app.POST(util.BasePath+"/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.GET("/download", handler.DownloadClient(db), handler.ValidSession) | 	app.POST(util.BasePath+"/send-telegram-client", handler.SendTelegramClient(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.GET("/wg-server", handler.WireGuardServer(db), handler.ValidSession) | 	app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.POST("wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession) | 	app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.POST("wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession) | 	app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession) | ||||||
| 	app.GET("/global-settings", handler.GlobalSettings(db), handler.ValidSession) | 	app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin) | ||||||
| 	app.POST("/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession) | 	app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | ||||||
| 	app.GET("/status", handler.Status(db), handler.ValidSession) | 	app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | ||||||
| 	app.GET("/api/clients", handler.GetClients(db), handler.ValidSession) | 	app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin) | ||||||
| 	app.GET("/api/client/:id", handler.GetClient(db), handler.ValidSession) | 	app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | ||||||
| 	app.GET("/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession) | 	app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession, handler.RefreshSession) | ||||||
| 	app.GET("/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession) | 	app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession) | ||||||
| 	app.GET("/api/apply-wg-config", handler.ApplyServerConfig(db, tmplBox), handler.ValidSession) | 	app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession) | ||||||
| 	app.GET("/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession) | 	app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession) | ||||||
| 	app.POST("/wake_on_lan_host", handler.SaveWakeOnLanHost(db), handler.ValidSession) | 	app.GET(util.BasePath+"/api/subnet-ranges", handler.GetOrderedSubnetRanges(), handler.ValidSession) | ||||||
| 	app.DELETE("/wake_on_lan_host/:mac_address", handler.DeleteWakeOnHost(db), handler.ValidSession) | 	app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession) | ||||||
| 	app.PUT("/wake_on_lan_host/:mac_address", handler.WakeOnHost(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) | ||||||
| 
 | 
 | ||||||
| 	// servers other static files
 | 	// strip the "assets/" prefix from the embedded directory so files can be called directly without the "assets/"
 | ||||||
| 	app.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler))) | 	// prefix
 | ||||||
|  | 	assetsDir, _ := fs.Sub(fs.FS(embeddedAssets), "assets") | ||||||
|  | 	assetHandler := http.FileServer(http.FS(assetsDir)) | ||||||
|  | 	// serves other static files
 | ||||||
|  | 	app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler))) | ||||||
| 
 | 
 | ||||||
| 	app.Logger.Fatal(app.Start(util.BindAddress)) | 	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 | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -11,10 +11,14 @@ type Client struct { | ||||||
| 	PublicKey       string    `json:"public_key"` | 	PublicKey       string    `json:"public_key"` | ||||||
| 	PresharedKey    string    `json:"preshared_key"` | 	PresharedKey    string    `json:"preshared_key"` | ||||||
| 	Name            string    `json:"name"` | 	Name            string    `json:"name"` | ||||||
|  | 	TgUserid        string    `json:"telegram_userid"` | ||||||
| 	Email           string    `json:"email"` | 	Email           string    `json:"email"` | ||||||
|  | 	SubnetRanges    []string  `json:"subnet_ranges,omitempty"` | ||||||
| 	AllocatedIPs    []string  `json:"allocated_ips"` | 	AllocatedIPs    []string  `json:"allocated_ips"` | ||||||
| 	AllowedIPs      []string  `json:"allowed_ips"` | 	AllowedIPs      []string  `json:"allowed_ips"` | ||||||
| 	ExtraAllowedIPs []string  `json:"extra_allowed_ips"` | 	ExtraAllowedIPs []string  `json:"extra_allowed_ips"` | ||||||
|  | 	Endpoint        string    `json:"endpoint"` | ||||||
|  | 	AdditionalNotes string    `json:"additional_notes"` | ||||||
| 	UseServerDNS    bool      `json:"use_server_dns"` | 	UseServerDNS    bool      `json:"use_server_dns"` | ||||||
| 	Enabled         bool      `json:"enabled"` | 	Enabled         bool      `json:"enabled"` | ||||||
| 	CreatedAt       time.Time `json:"created_at"` | 	CreatedAt       time.Time `json:"created_at"` | ||||||
|  | @ -26,3 +30,9 @@ type ClientData struct { | ||||||
| 	Client *Client | 	Client *Client | ||||||
| 	QRCode string | 	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 { | type BaseData struct { | ||||||
| 	Active      string | 	Active      string | ||||||
| 	CurrentUser 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
 | 	ListenPort int       `json:"listen_port,string"` // ,string to get listen_port string input as int
 | ||||||
| 	UpdatedAt  time.Time `json:"updated_at"` | 	UpdatedAt  time.Time `json:"updated_at"` | ||||||
| 	PostUp     string    `json:"post_up"` | 	PostUp     string    `json:"post_up"` | ||||||
|  | 	PreDown    string    `json:"pre_down"` | ||||||
| 	PostDown   string    `json:"post_down"` | 	PostDown   string    `json:"post_down"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,6 +10,8 @@ type GlobalSetting struct { | ||||||
| 	DNSServers          []string  `json:"dns_servers"` | 	DNSServers          []string  `json:"dns_servers"` | ||||||
| 	MTU                 int       `json:"mtu,string"` | 	MTU                 int       `json:"mtu,string"` | ||||||
| 	PersistentKeepalive int       `json:"persistent_keepalive,string"` | 	PersistentKeepalive int       `json:"persistent_keepalive,string"` | ||||||
|  | 	FirewallMark        string    `json:"firewall_mark"` | ||||||
|  | 	Table               string    `json:"table"` | ||||||
| 	ConfigFilePath      string    `json:"config_file_path"` | 	ConfigFilePath      string    `json:"config_file_path"` | ||||||
| 	UpdatedAt           time.Time `json:"updated_at"` | 	UpdatedAt           time.Time `json:"updated_at"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,4 +4,7 @@ package model | ||||||
| type User struct { | type User struct { | ||||||
| 	Username string `json:"username"` | 	Username string `json:"username"` | ||||||
| 	Password string `json:"password"` | 	Password string `json:"password"` | ||||||
|  | 	// PasswordHash takes precedence over Password.
 | ||||||
|  | 	PasswordHash string `json:"password_hash"` | ||||||
|  | 	Admin        bool   `json:"admin"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ package model | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"net" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
|  | @ -18,7 +19,13 @@ func (host WakeOnLanHost) ResolveResourceName() (string, error) { | ||||||
| 		return "", errors.New("mac Address is Empty") | 		return "", errors.New("mac Address is Empty") | ||||||
| 	} | 	} | ||||||
| 	resourceName = strings.ToUpper(resourceName) | 	resourceName = strings.ToUpper(resourceName) | ||||||
| 	return strings.ReplaceAll(resourceName, ":", "-"), nil | 	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" | const WakeOnLanHostCollectionName = "wake_on_lan_hosts" | ||||||
|  |  | ||||||
|  | @ -3,21 +3,23 @@ package router | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"io" | 	"io" | ||||||
|  | 	"io/fs" | ||||||
| 	"reflect" | 	"reflect" | ||||||
|  | 	"strings" | ||||||
| 	"text/template" | 	"text/template" | ||||||
| 
 | 
 | ||||||
| 	rice "github.com/GeertJohan/go.rice" |  | ||||||
| 	"github.com/gorilla/sessions" | 	"github.com/gorilla/sessions" | ||||||
| 	"github.com/labstack/echo-contrib/session" | 	"github.com/labstack/echo-contrib/session" | ||||||
| 	"github.com/labstack/echo/v4" | 	"github.com/labstack/echo/v4" | ||||||
| 	"github.com/labstack/echo/v4/middleware" | 	"github.com/labstack/echo/v4/middleware" | ||||||
| 	"github.com/labstack/gommon/log" | 	"github.com/labstack/gommon/log" | ||||||
|  | 	"github.com/ngoduykhanh/wireguard-ui/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // TemplateRegistry is a custom html/template renderer for Echo framework
 | // TemplateRegistry is a custom html/template renderer for Echo framework
 | ||||||
| type TemplateRegistry struct { | type TemplateRegistry struct { | ||||||
| 	templates map[string]*template.Template | 	templates map[string]*template.Template | ||||||
| 	extraData map[string]string | 	extraData map[string]interface{} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Render e.Renderer 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 { | 		for k, v := range t.extraData { | ||||||
| 			data.(map[string]interface{})[k] = v | 			data.(map[string]interface{})[k] = v | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		data.(map[string]interface{})["client_defaults"] = util.ClientDefaultsFromEnv() | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// login page does not need the base layout
 | 	// login page does not need the base layout
 | ||||||
|  | @ -44,64 +48,106 @@ func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // New function
 | // 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 := 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
 | 	// read html template file to string
 | ||||||
| 	tmplBaseString, err := tmplBox.String("base.html") | 	tmplBaseString, err := util.StringFromEmbedFile(tmplDir, "base.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplLoginString, err := tmplBox.String("login.html") | 	tmplLoginString, err := util.StringFromEmbedFile(tmplDir, "login.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplClientsString, err := tmplBox.String("clients.html") | 	tmplProfileString, err := util.StringFromEmbedFile(tmplDir, "profile.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplServerString, err := tmplBox.String("server.html") | 	tmplClientsString, err := util.StringFromEmbedFile(tmplDir, "clients.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplGlobalSettingsString, err := tmplBox.String("global_settings.html") | 	tmplServerString, err := util.StringFromEmbedFile(tmplDir, "server.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplStatusString, err := tmplBox.String("status.html") | 	tmplGlobalSettingsString, err := util.StringFromEmbedFile(tmplDir, "global_settings.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplWakeOnLanHostsString, err := tmplBox.String("wake_on_lan_hosts.html") | 	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 { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// create template list
 | 	// create template list
 | ||||||
|  | 	funcs := template.FuncMap{ | ||||||
|  | 		"StringsJoin": strings.Join, | ||||||
|  | 	} | ||||||
| 	templates := make(map[string]*template.Template) | 	templates := make(map[string]*template.Template) | ||||||
| 	templates["login.html"] = template.Must(template.New("login").Parse(tmplLoginString)) | 	templates["login.html"] = template.Must(template.New("login").Funcs(funcs).Parse(tmplLoginString)) | ||||||
| 	templates["clients.html"] = template.Must(template.New("clients").Parse(tmplBaseString + tmplClientsString)) | 	templates["profile.html"] = template.Must(template.New("profile").Funcs(funcs).Parse(tmplBaseString + tmplProfileString)) | ||||||
| 	templates["server.html"] = template.Must(template.New("server").Parse(tmplBaseString + tmplServerString)) | 	templates["clients.html"] = template.Must(template.New("clients").Funcs(funcs).Parse(tmplBaseString + tmplClientsString)) | ||||||
| 	templates["global_settings.html"] = template.Must(template.New("global_settings").Parse(tmplBaseString + tmplGlobalSettingsString)) | 	templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString)) | ||||||
| 	templates["status.html"] = template.Must(template.New("status").Parse(tmplBaseString + tmplStatusString)) | 	templates["global_settings.html"] = template.Must(template.New("global_settings").Funcs(funcs).Parse(tmplBaseString + tmplGlobalSettingsString)) | ||||||
| 	templates["wake_on_lan_hosts.html"] = template.Must(template.New("wake_on_lan_hosts").Parse(tmplBaseString + tmplWakeOnLanHostsString)) | 	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.Pre(middleware.RemoveTrailingSlash()) | ||||||
| 	e.Use(middleware.Logger()) | 	e.Use(middleware.LoggerWithConfig(logConfig)) | ||||||
| 	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.HideBanner = true | 	e.HideBanner = true | ||||||
|  | 	e.HidePort = lvl > log.INFO // hide the port output if the log level is higher than INFO
 | ||||||
| 	e.Validator = NewValidator() | 	e.Validator = NewValidator() | ||||||
| 	e.Renderer = &TemplateRegistry{ | 	e.Renderer = &TemplateRegistry{ | ||||||
| 		templates: templates, | 		templates: templates, | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
|  | 	"strconv" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sdomino/scribble" | 	"github.com/sdomino/scribble" | ||||||
|  | @ -32,17 +33,18 @@ func New(dbPath string) (*JsonDB, error) { | ||||||
| 		dbPath: dbPath, | 		dbPath: dbPath, | ||||||
| 	} | 	} | ||||||
| 	return &ans, nil | 	return &ans, nil | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (o *JsonDB) Init() error { | func (o *JsonDB) Init() error { | ||||||
| 	var clientPath string = path.Join(o.dbPath, "clients") | 	var clientPath = path.Join(o.dbPath, "clients") | ||||||
| 	var serverPath string = path.Join(o.dbPath, "server") | 	var serverPath = path.Join(o.dbPath, "server") | ||||||
| 	var wakeOnLanHostsPath string = path.Join(o.dbPath, "wake_on_lan_hosts") | 	var userPath = path.Join(o.dbPath, "users") | ||||||
| 	var serverInterfacePath string = path.Join(serverPath, "interfaces.json") | 	var wakeOnLanHostsPath = path.Join(o.dbPath, "wake_on_lan_hosts") | ||||||
| 	var serverKeyPairPath string = path.Join(serverPath, "keypair.json") | 	var serverInterfacePath = path.Join(serverPath, "interfaces.json") | ||||||
| 	var globalSettingPath string = path.Join(serverPath, "global_settings.json") | 	var serverKeyPairPath = path.Join(serverPath, "keypair.json") | ||||||
| 	var userPath string = path.Join(serverPath, "users.json") | 	var globalSettingPath = path.Join(serverPath, "global_settings.json") | ||||||
|  | 	var hashesPath = path.Join(serverPath, "hashes.json") | ||||||
|  | 
 | ||||||
| 	// create directories if they do not exist
 | 	// create directories if they do not exist
 | ||||||
| 	if _, err := os.Stat(clientPath); os.IsNotExist(err) { | 	if _, err := os.Stat(clientPath); os.IsNotExist(err) { | ||||||
| 		os.MkdirAll(clientPath, os.ModePerm) | 		os.MkdirAll(clientPath, os.ModePerm) | ||||||
|  | @ -50,6 +52,9 @@ func (o *JsonDB) Init() error { | ||||||
| 	if _, err := os.Stat(serverPath); os.IsNotExist(err) { | 	if _, err := os.Stat(serverPath); os.IsNotExist(err) { | ||||||
| 		os.MkdirAll(serverPath, os.ModePerm) | 		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) { | 	if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) { | ||||||
| 		os.MkdirAll(wakeOnLanHostsPath, os.ModePerm) | 		os.MkdirAll(wakeOnLanHostsPath, os.ModePerm) | ||||||
| 	} | 	} | ||||||
|  | @ -57,15 +62,20 @@ func (o *JsonDB) Init() error { | ||||||
| 	// server's interface
 | 	// server's interface
 | ||||||
| 	if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) { | 	if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) { | ||||||
| 		serverInterface := new(model.ServerInterface) | 		serverInterface := new(model.ServerInterface) | ||||||
| 		serverInterface.Addresses = []string{util.DefaultServerAddress} | 		serverInterface.Addresses = util.LookupEnvOrStrings(util.ServerAddressesEnvVar, []string{util.DefaultServerAddress}) | ||||||
| 		serverInterface.ListenPort = util.DefaultServerPort | 		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() | 		serverInterface.UpdatedAt = time.Now().UTC() | ||||||
| 		o.conn.Write("server", "interfaces", serverInterface) | 		o.conn.Write("server", "interfaces", serverInterface) | ||||||
|  | 		err := util.ManagePerms(serverInterfacePath) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// server's key pair
 | 	// server's key pair
 | ||||||
| 	if _, err := os.Stat(serverKeyPairPath); os.IsNotExist(err) { | 	if _, err := os.Stat(serverKeyPairPath); os.IsNotExist(err) { | ||||||
| 
 |  | ||||||
| 		key, err := wgtypes.GeneratePrivateKey() | 		key, err := wgtypes.GeneratePrivateKey() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return scribble.ErrMissingCollection | 			return scribble.ErrMissingCollection | ||||||
|  | @ -75,41 +85,152 @@ func (o *JsonDB) Init() error { | ||||||
| 		serverKeyPair.PublicKey = key.PublicKey().String() | 		serverKeyPair.PublicKey = key.PublicKey().String() | ||||||
| 		serverKeyPair.UpdatedAt = time.Now().UTC() | 		serverKeyPair.UpdatedAt = time.Now().UTC() | ||||||
| 		o.conn.Write("server", "keypair", serverKeyPair) | 		o.conn.Write("server", "keypair", serverKeyPair) | ||||||
|  | 		err = util.ManagePerms(serverKeyPairPath) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// global settings
 | 	// global settings
 | ||||||
| 	if _, err := os.Stat(globalSettingPath); os.IsNotExist(err) { | 	if _, err := os.Stat(globalSettingPath); os.IsNotExist(err) { | ||||||
| 
 | 		endpointAddress := util.LookupEnvOrString(util.EndpointAddressEnvVar, "") | ||||||
| 		publicInterface, err := util.GetPublicIP() | 		if endpointAddress == "" { | ||||||
| 		if err != nil { | 			// automatically find an external IP address
 | ||||||
| 			return err | 			publicInterface, err := util.GetPublicIP() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			endpointAddress = publicInterface.IPAddress | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		globalSetting := new(model.GlobalSetting) | 		globalSetting := new(model.GlobalSetting) | ||||||
| 		globalSetting.EndpointAddress = publicInterface.IPAddress | 		globalSetting.EndpointAddress = endpointAddress | ||||||
| 		globalSetting.DNSServers = []string{util.DefaultDNS} | 		globalSetting.DNSServers = util.LookupEnvOrStrings(util.DNSEnvVar, []string{util.DefaultDNS}) | ||||||
| 		globalSetting.MTU = util.DefaultMTU | 		globalSetting.MTU = util.LookupEnvOrInt(util.MTUEnvVar, util.DefaultMTU) | ||||||
| 		globalSetting.PersistentKeepalive = util.DefaultPersistentKeepalive | 		globalSetting.PersistentKeepalive = util.LookupEnvOrInt(util.PersistentKeepaliveEnvVar, util.DefaultPersistentKeepalive) | ||||||
| 		globalSetting.ConfigFilePath = util.DefaultConfigFilePath | 		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() | 		globalSetting.UpdatedAt = time.Now().UTC() | ||||||
| 		o.conn.Write("server", "global_settings", globalSetting) | 		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
 | 	// 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 := new(model.User) | ||||||
| 		user.Username = util.GetCredVar(util.UsernameEnvVar, util.DefaultUsername) | 		user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername) | ||||||
| 		user.Password = util.GetCredVar(util.PasswordEnvVar, util.DefaultPassword) | 		user.Admin = util.DefaultIsAdmin | ||||||
| 		o.conn.Write("server", "users", user) | 		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 | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetUser func to query user info from the database
 | // GetUsers func to get all users from the database
 | ||||||
| func (o *JsonDB) GetUser() (model.User, error) { | 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{} | 	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
 | // GetGlobalSettings func to query global settings from the database
 | ||||||
|  | @ -118,7 +239,7 @@ func (o *JsonDB) GetGlobalSettings() (model.GlobalSetting, error) { | ||||||
| 	return settings, o.conn.Read("server", "global_settings", &settings) | 	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) { | func (o *JsonDB) GetServer() (model.Server, error) { | ||||||
| 	server := model.Server{} | 	server := model.Server{} | ||||||
| 	// read server interface information
 | 	// read server interface information
 | ||||||
|  | @ -142,7 +263,7 @@ func (o *JsonDB) GetServer() (model.Server, error) { | ||||||
| func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) { | func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) { | ||||||
| 	var clients []model.ClientData | 	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") | 	records, err := o.conn.ReadAll("clients") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return clients, err | 		return clients, err | ||||||
|  | @ -154,7 +275,7 @@ func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) { | ||||||
| 		clientData := model.ClientData{} | 		clientData := model.ClientData{} | ||||||
| 
 | 
 | ||||||
| 		// get client info
 | 		// 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) | 			return clients, fmt.Errorf("cannot decode client json structure: %v", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -165,7 +286,7 @@ func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) { | ||||||
| 
 | 
 | ||||||
| 			png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256) | 			png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256) | ||||||
| 			if err == nil { | 			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 { | 			} else { | ||||||
| 				fmt.Print("Cannot generate QR code: ", err) | 				fmt.Print("Cannot generate QR code: ", err) | ||||||
| 			} | 			} | ||||||
|  | @ -179,7 +300,7 @@ func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) { | ||||||
| 	return clients, nil | 	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{} | 	client := model.Client{} | ||||||
| 	clientData := model.ClientData{} | 	clientData := model.ClientData{} | ||||||
| 
 | 
 | ||||||
|  | @ -189,13 +310,20 @@ func (o *JsonDB) GetClientByID(clientID string, hasQRCode bool) (model.ClientDat | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// generate client qrcode image in base64
 | 	// generate client qrcode image in base64
 | ||||||
| 	if hasQRCode && client.PrivateKey != "" { | 	if qrCodeSettings.Enabled && client.PrivateKey != "" { | ||||||
| 		server, _ := o.GetServer() | 		server, _ := o.GetServer() | ||||||
| 		globalSettings, _ := o.GetGlobalSettings() | 		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) | 		png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256) | ||||||
| 		if err == nil { | 		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 { | 		} else { | ||||||
| 			fmt.Print("Cannot generate QR code: ", err) | 			fmt.Print("Cannot generate QR code: ", err) | ||||||
| 		} | 		} | ||||||
|  | @ -207,21 +335,76 @@ func (o *JsonDB) GetClientByID(clientID string, hasQRCode bool) (model.ClientDat | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (o *JsonDB) SaveClient(client model.Client) error { | 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 { | func (o *JsonDB) DeleteClient(clientID string) error { | ||||||
|  | 	util.RemoveTgToClientID(clientID) | ||||||
| 	return o.conn.Delete("clients", clientID) | 	return o.conn.Delete("clients", clientID) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (o *JsonDB) SaveServerInterface(serverInterface model.ServerInterface) error { | 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 { | 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 { | 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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,10 @@ package jsondb | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"path" | ||||||
|  | 
 | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/model" | 	"github.com/ngoduykhanh/wireguard-ui/model" | ||||||
|  | 	"github.com/ngoduykhanh/wireguard-ui/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (o *JsonDB) GetWakeOnLanHosts() ([]model.WakeOnLanHost, error) { | func (o *JsonDB) GetWakeOnLanHosts() ([]model.WakeOnLanHost, error) { | ||||||
|  | @ -20,7 +23,7 @@ func (o *JsonDB) GetWakeOnLanHosts() ([]model.WakeOnLanHost, error) { | ||||||
| 		host := model.WakeOnLanHost{} | 		host := model.WakeOnLanHost{} | ||||||
| 
 | 
 | ||||||
| 		// get client info
 | 		// get client info
 | ||||||
| 		if err := json.Unmarshal([]byte(f), &host); err != nil { | 		if err := json.Unmarshal(f, &host); err != nil { | ||||||
| 			return hosts, fmt.Errorf("cannot decode client json structure: %v", err) | 			return hosts, fmt.Errorf("cannot decode client json structure: %v", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -65,7 +68,14 @@ func (o *JsonDB) SaveWakeOnLanHost(host model.WakeOnLanHost) error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return o.conn.Write(model.WakeOnLanHostCollectionName, resourceName, host) | 	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 { | func (o *JsonDB) DeleteWakeOnHost(host model.WakeOnLanHost) error { | ||||||
|  |  | ||||||
|  | @ -6,11 +6,14 @@ import ( | ||||||
| 
 | 
 | ||||||
| type IStore interface { | type IStore interface { | ||||||
| 	Init() error | 	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) | 	GetGlobalSettings() (model.GlobalSetting, error) | ||||||
| 	GetServer() (model.Server, error) | 	GetServer() (model.Server, error) | ||||||
| 	GetClients(hasQRCode bool) ([]model.ClientData, 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 | 	SaveClient(client model.Client) error | ||||||
| 	DeleteClient(clientID string) error | 	DeleteClient(clientID string) error | ||||||
| 	SaveServerInterface(serverInterface model.ServerInterface) error | 	SaveServerInterface(serverInterface model.ServerInterface) error | ||||||
|  | @ -21,4 +24,7 @@ type IStore interface { | ||||||
| 	DeleteWakeOnHostLanHost(macAddress string) error | 	DeleteWakeOnHostLanHost(macAddress string) error | ||||||
| 	SaveWakeOnLanHost(host model.WakeOnLanHost) error | 	SaveWakeOnLanHost(host model.WakeOnLanHost) error | ||||||
| 	DeleteWakeOnHost(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> |     <title>{{template "title" .}}</title> | ||||||
|     <!-- Tell the browser to be responsive to screen width --> |     <!-- Tell the browser to be responsive to screen width --> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> |     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|  |     <!-- Favicon --> | ||||||
|  |     <link rel="icon" href="{{.basePath}}/favicon"> | ||||||
| 
 | 
 | ||||||
|     <!-- Font Awesome --> |     <!-- 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 --> |     <!-- 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 --> |     <!-- 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 --> |     <!-- 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 --> |     <!-- 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 --> |     <!-- Ionicons --> | ||||||
|     <link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"> |     <link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"> | ||||||
|     <!-- overlayScrollbars --> |     <!-- 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 --> |     <!-- Google Font: Source Sans Pro --> | ||||||
|     <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet"> |     <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet"> | ||||||
| 
 | 
 | ||||||
|  | @ -44,28 +46,39 @@ | ||||||
|             </ul> |             </ul> | ||||||
| 
 | 
 | ||||||
|             <!-- SEARCH FORM --> |             <!-- SEARCH FORM --> | ||||||
| <!--            <form class="form-inline ml-3">--> |             <form class="form-inline ml-3" style="display: none" id="search-form"> | ||||||
| <!--                <div class="input-group input-group-sm">--> |                 <div class="input-group input-group-sm"> | ||||||
| <!--                    <input class="form-control form-control-navbar" type="search" placeholder="Search"--> |                     <input class="form-control form-control-navbar" placeholder="Search" | ||||||
| <!--                        aria-label="Search">--> |                         aria-label="Search" id="search-input"> | ||||||
| <!--                    <div class="input-group-append">--> |                     <div class="input-group-append"> | ||||||
| <!--                        <button class="btn btn-navbar" type="submit">--> |                         <button class="btn-navbar" type="submit" disabled> | ||||||
| <!--                            <i class="fas fa-search"></i>--> |                             <i class="fas fa-search"></i> | ||||||
| <!--                        </button>--> |                         </button> | ||||||
| <!--                    </div>--> |                     </div> | ||||||
| <!--                </div>--> |                 </div> | ||||||
| <!--            </form>--> |                 <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 --> |             <!-- Right navbar links --> | ||||||
|             <div class="navbar-nav ml-auto"> |             <div class="navbar-nav ml-auto"> | ||||||
|                 <button style="margin-left: 0.5em;" type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" |                 <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 |                     data-target="#modal_new_client"><i class="nav-icon fas fa-plus"></i> New | ||||||
|                     Client</button> |                     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 |                     data-target="#modal_apply_config"><i class="nav-icon fas fa-check"></i> Apply | ||||||
|                     Config</button> |                     Config</button> | ||||||
|                 {{if .baseData.CurrentUser}} |                 {{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> |                     class="btn btn-outline-danger btn-sm"><i class="nav-icon fas fa-sign-out-alt"></i> Logout</button> | ||||||
|                 {{end}} |                 {{end}} | ||||||
|             </div> |             </div> | ||||||
|  | @ -75,7 +88,7 @@ | ||||||
|         <!-- Main Sidebar Container --> |         <!-- Main Sidebar Container --> | ||||||
|         <aside class="main-sidebar sidebar-dark-primary elevation-4"> |         <aside class="main-sidebar sidebar-dark-primary elevation-4"> | ||||||
|             <!-- Brand Logo --> |             <!-- Brand Logo --> | ||||||
|             <a href="/" class="brand-link"> |             <a href="{{.basePath}}" class="brand-link"> | ||||||
|                 <span class="brand-text">  WIREGUARD UI</span> |                 <span class="brand-text">  WIREGUARD UI</span> | ||||||
|             </a> |             </a> | ||||||
| 
 | 
 | ||||||
|  | @ -87,7 +100,17 @@ | ||||||
|                         <i class="nav-icon fas fa-2x fa-user"></i> |                         <i class="nav-icon fas fa-2x fa-user"></i> | ||||||
|                     </div> |                     </div> | ||||||
|                     <div class="info"> |                     <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> | ||||||
|                 </div> |                 </div> | ||||||
| 
 | 
 | ||||||
|  | @ -96,33 +119,49 @@ | ||||||
|                     <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false"> |                     <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-header">MAIN</li> | ||||||
|                         <li class="nav-item"> |                         <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> |                                 <i class="nav-icon fas fa-user-secret"></i> | ||||||
|                                 <p> |                                 <p> | ||||||
|                                     Wireguard Clients |                                     Wireguard Clients | ||||||
|                                 </p> |                                 </p> | ||||||
|                             </a> |                             </a> | ||||||
|                         </li> |                         </li> | ||||||
|  | 
 | ||||||
|  |                         {{if .baseData.Admin}} | ||||||
|                         <li class="nav-item"> |                         <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> |                                 <i class="nav-icon fas fa-server"></i> | ||||||
|                                 <p> |                                 <p> | ||||||
|                                     Wireguard Server |                                     Wireguard Server | ||||||
|                                 </p> |                                 </p> | ||||||
|                             </a> |                             </a> | ||||||
|                         </li> |                         </li> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|                         <li class="nav-header">SETTINGS</li> |                         <li class="nav-header">SETTINGS</li> | ||||||
|                         <li class="nav-item"> |                         <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> |                                 <i class="nav-icon fas fa-cog"></i> | ||||||
|                                 <p> |                                 <p> | ||||||
|                                     Global Settings |                                     Global Settings | ||||||
|                                 </p> |                                 </p> | ||||||
|                             </a> |                             </a> | ||||||
|                         </li> |                         </li> | ||||||
|  |                         {{if not .loginDisabled}} | ||||||
|  |                         <li class="nav-item"> | ||||||
|  |                             <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-header">UTILITIES</li> | ||||||
|                         <li class="nav-item"> |                         <li class="nav-item"> | ||||||
|                             <a href="/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}"> |                             <a href="{{.basePath}}/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}"> | ||||||
|                                 <i class="nav-icon fas fa-signal"></i> |                                 <i class="nav-icon fas fa-signal"></i> | ||||||
|                                 <p> |                                 <p> | ||||||
|                                     Status |                                     Status | ||||||
|  | @ -130,13 +169,22 @@ | ||||||
|                             </a> |                             </a> | ||||||
|                         </li> |                         </li> | ||||||
|                         <li class="nav-item"> |                         <li class="nav-item"> | ||||||
|                             <a href="/wake_on_lan_hosts" class="nav-link {{if eq .baseData.Active "wake_on_lan_hosts" }}active{{end}}"> |                             <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> |                                 <i class="nav-icon fas  fa-solid fa-power-off"></i> | ||||||
|                                 <p> |                                 <p> | ||||||
|                                     WoL Hosts |                                     WoL Hosts | ||||||
|                                 </p> |                                 </p> | ||||||
|                             </a> |                             </a> | ||||||
|                         </li> |                         </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> |                     </ul> | ||||||
|                 </nav> |                 </nav> | ||||||
|                 <!-- /.sidebar-menu --> |                 <!-- /.sidebar-menu --> | ||||||
|  | @ -163,6 +211,12 @@ | ||||||
|                                 <label for="client_email" class="control-label">Email</label> |                                 <label for="client_email" class="control-label">Email</label> | ||||||
|                                 <input type="text" class="form-control" id="client_email" name="client_email"> |                                 <input type="text" class="form-control" id="client_email" name="client_email"> | ||||||
|                             </div> |                             </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"> |                             <div class="form-group"> | ||||||
|                                 <label for="client_allocated_ips" class="control-label">IP Allocation</label> |                                 <label for="client_allocated_ips" class="control-label">IP Allocation</label> | ||||||
|                                 <input type="text" data-role="tagsinput" class="form-control" id="client_allocated_ips"> |                                 <input type="text" data-role="tagsinput" class="form-control" id="client_allocated_ips"> | ||||||
|  | @ -175,7 +229,7 @@ | ||||||
|                                     </i> |                                     </i> | ||||||
|                                 </label> |                                 </label> | ||||||
|                                 <input type="text" data-role="tagsinput" class="form-control" id="client_allowed_ips" |                                 <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> | ||||||
|                             <div class="form-group"> |                             <div class="form-group"> | ||||||
|                                 <label for="client_extra_allowed_ips" class="control-label">Extra Allowed IPs |                                 <label for="client_extra_allowed_ips" class="control-label">Extra Allowed IPs | ||||||
|  | @ -184,11 +238,15 @@ | ||||||
|                                        client. These addresses will be included in 'AllowedIPs' of WG server config"> |                                        client. These addresses will be included in 'AllowedIPs' of WG server config"> | ||||||
|                                     </i> |                                     </i> | ||||||
|                                 </label> |                                 </label> | ||||||
|                                 <input type="text" data-role="tagsinput" class="form-control" id="client_extra_allowed_ips"> |                                 <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> | ||||||
|                             <div class="form-group"> |                             <div class="form-group"> | ||||||
|                                 <div class="icheck-primary d-inline"> |                                 <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"> |                                     <label for="use_server_dns"> | ||||||
|                                         Use server DNS |                                         Use server DNS | ||||||
|                                     </label> |                                     </label> | ||||||
|  | @ -196,7 +254,7 @@ | ||||||
|                             </div> |                             </div> | ||||||
|                             <div class="form-group"> |                             <div class="form-group"> | ||||||
|                                 <div class="icheck-primary d-inline"> |                                 <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"> |                                     <label for="enabled"> | ||||||
|                                         Enable after creation |                                         Enable after creation | ||||||
|                                     </label> |                                     </label> | ||||||
|  | @ -206,7 +264,7 @@ | ||||||
|                                 <summary><strong>Public and Preshared Keys</strong> |                                 <summary><strong>Public and Preshared Keys</strong> | ||||||
|                                     <i class="fas fa-info-circle" data-toggle="tooltip" |                                     <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 |                                        data-original-title="If you don't want to let the server generate and store the | ||||||
|                                        client's private, you can manually specify its public and preshared key here |                                        client's private key, you can manually specify its public and preshared key here | ||||||
|                                        . Note: QR code will not be generated"> |                                        . Note: QR code will not be generated"> | ||||||
|                                     </i> |                                     </i> | ||||||
|                                 </summary> |                                 </summary> | ||||||
|  | @ -220,7 +278,19 @@ | ||||||
|                                     <label for="client_preshared_key" class="control-label"> |                                     <label for="client_preshared_key" class="control-label"> | ||||||
|                                         Preshared Key |                                         Preshared Key | ||||||
|                                     </label> |                                     </label> | ||||||
|                                     <input type="text" class="form-control" id="client_preshared_key" name="client_preshared_key" placeholder="Autogenerated"> |                                     <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> |                                 </div> | ||||||
|                             </details> |                             </details> | ||||||
|                         </div> |                         </div> | ||||||
|  | @ -277,7 +347,7 @@ | ||||||
|             <!-- /.content --> |             <!-- /.content --> | ||||||
|         </div> |         </div> | ||||||
|         <!-- /.content-wrapper --> |         <!-- /.content-wrapper --> | ||||||
| 
 |         <!-- | ||||||
|         <footer class="main-footer"> |         <footer class="main-footer"> | ||||||
|             <div class="float-right d-none d-sm-block"> |             <div class="float-right d-none d-sm-block"> | ||||||
|                 <b>Version</b> {{ .appVersion }} |                 <b>Version</b> {{ .appVersion }} | ||||||
|  | @ -285,7 +355,7 @@ | ||||||
|             <strong>Copyright © <script>document.write(new Date().getFullYear())</script> <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. |             reserved. | ||||||
|         </footer> |         </footer> | ||||||
| 
 |         --> | ||||||
|         <!-- Control Sidebar --> |         <!-- Control Sidebar --> | ||||||
|         <aside class="control-sidebar control-sidebar-dark"> |         <aside class="control-sidebar control-sidebar-dark"> | ||||||
|             <!-- Control sidebar content goes here --> |             <!-- Control sidebar content goes here --> | ||||||
|  | @ -295,34 +365,86 @@ | ||||||
|     <!-- ./wrapper --> |     <!-- ./wrapper --> | ||||||
| 
 | 
 | ||||||
|     <!-- jQuery --> |     <!-- jQuery --> | ||||||
|     <script src="static/plugins/jquery/jquery.min.js"></script> |     <script src="{{.basePath}}/static/plugins/jquery/jquery.min.js"></script> | ||||||
|     <!-- Bootstrap 4 --> |     <!-- 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 --> |     <!-- 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 --> |     <!-- 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 --> |     <!-- Toastr --> | ||||||
|     <script src="static/plugins/toastr/toastr.min.js"></script> |     <script src="{{.basePath}}/static/plugins/toastr/toastr.min.js"></script> | ||||||
|     <!-- Jquery Tags Input --> |     <!-- 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 --> |     <!-- AdminLTE App --> | ||||||
|     <script src="static/dist/js/adminlte.min.js"></script> |     <script src="{{.basePath}}/static/dist/js/adminlte.min.js"></script> | ||||||
|     <!-- Custom js --> |     <!-- Custom js --> | ||||||
|     <script src="static/custom/js/helper.js"></script> |     <script src="{{.basePath}}/static/custom/js/helper.js"></script> | ||||||
|     <script> |     <script> | ||||||
|         // initialize all tooltips |         // initialize all tooltips | ||||||
|         $(function () { |         $(function () { | ||||||
|             $('[data-toggle="tooltip"]').tooltip() |             $('[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 |         // populateClient function for render new client info | ||||||
|         // on the client page. |         // on the client page. | ||||||
|         function populateClient(client_id) { |         function populateClient(client_id) { | ||||||
|             $.ajax({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'GET', |                 method: 'GET', | ||||||
|                 url: '/api/client/' + client_id, |                 url: '{{.basePath}}/api/client/' + client_id, | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 success: function (resp) { |                 success: function (resp) { | ||||||
|  | @ -339,8 +461,10 @@ | ||||||
|         function submitNewClient() { |         function submitNewClient() { | ||||||
|             const name = $("#client_name").val(); |             const name = $("#client_name").val(); | ||||||
|             const email = $("#client_email").val(); |             const email = $("#client_email").val(); | ||||||
|  |             const telegram_userid = $("#client_telegram_userid").val(); | ||||||
|             const allocated_ips = $("#client_allocated_ips").val().split(","); |             const allocated_ips = $("#client_allocated_ips").val().split(","); | ||||||
|             const allowed_ips = $("#client_allowed_ips").val().split(","); |             const allowed_ips = $("#client_allowed_ips").val().split(","); | ||||||
|  |             const endpoint = $("#client_endpoint").val(); | ||||||
|             let use_server_dns = false; |             let use_server_dns = false; | ||||||
|             let extra_allowed_ips = []; |             let extra_allowed_ips = []; | ||||||
| 
 | 
 | ||||||
|  | @ -361,14 +485,16 @@ | ||||||
|             const public_key = $("#client_public_key").val(); |             const public_key = $("#client_public_key").val(); | ||||||
|             const preshared_key = $("#client_preshared_key").val(); |             const preshared_key = $("#client_preshared_key").val(); | ||||||
|              |              | ||||||
|             const data = {"name": name, "email": email, "allocated_ips": allocated_ips, "allowed_ips": allowed_ips, |             const additional_notes = $("#additional_notes").val(); | ||||||
|                 "extra_allowed_ips": extra_allowed_ips, "use_server_dns": use_server_dns, "enabled": enabled, | 
 | ||||||
|                 "public_key": public_key, "preshared_key": preshared_key}; |             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({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'POST', |                 method: 'POST', | ||||||
|                 url: '/new-client', |                 url: '{{.basePath}}/new-client', | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 data: JSON.stringify(data), |                 data: JSON.stringify(data), | ||||||
|  | @ -376,9 +502,10 @@ | ||||||
|                     $("#modal_new_client").modal('hide'); |                     $("#modal_new_client").modal('hide'); | ||||||
|                     toastr.success('Created new client successfully'); |                     toastr.success('Created new client successfully'); | ||||||
|                     // Update the home page (clients page) after adding successfully |                     // Update the home page (clients page) after adding successfully | ||||||
|                     if (window.location.pathname === "/") { |                     if (window.location.pathname === "{{.basePath}}/") { | ||||||
|                         populateClient(resp.id); |                         populateClient(resp.id); | ||||||
|                     } |                     } | ||||||
|  |                     updateApplyConfigVisibility() | ||||||
|                 }, |                 }, | ||||||
|                 error: function(jqXHR, exception) { |                 error: function(jqXHR, exception) { | ||||||
|                     const responseJson = jQuery.parseJSON(jqXHR.responseText); |                     const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||||
|  | @ -389,19 +516,32 @@ | ||||||
| 
 | 
 | ||||||
|         // updateIPAllocationSuggestion function for automatically fill |         // updateIPAllocationSuggestion function for automatically fill | ||||||
|         // the IP Allocation input with suggested ip addresses |         // 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({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'GET', |                 method: 'GET', | ||||||
|                 url: '/api/suggest-client-ips', |                 url: `{{.basePath}}/api/suggest-client-ips?sr=${subnetRange}`, | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 success: function(data) { |                 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) { |                     data.forEach(function (item, index) { | ||||||
|                         $('#client_allocated_ips').addTag(item); |                         $('#client_allocated_ips').addTag(item); | ||||||
|                     }) |                     }) | ||||||
|                 }, |                 }, | ||||||
|                 error: function(jqXHR, exception) { |                 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); |                     const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||||
|                     toastr.error(responseJson['message']); |                     toastr.error(responseJson['message']); | ||||||
|                 } |                 } | ||||||
|  | @ -420,6 +560,7 @@ | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|  |             'minInputWidth': '100%', | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -431,6 +572,7 @@ | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|  |             'minInputWidth': '100%', | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -441,6 +583,7 @@ | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|  |             'minInputWidth': '100%', | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -485,20 +628,31 @@ | ||||||
|                 $("#client_preshared_key").val(""); |                 $("#client_preshared_key").val(""); | ||||||
|                 $("#client_allocated_ips").importTags(''); |                 $("#client_allocated_ips").importTags(''); | ||||||
|                 $("#client_extra_allowed_ips").importTags(''); |                 $("#client_extra_allowed_ips").importTags(''); | ||||||
|                 updateIPAllocationSuggestion(); |                 $("#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 |         // apply_config_confirm button event | ||||||
|         $(document).ready(function () { |         $(document).ready(function () { | ||||||
|             $("#apply_config_confirm").click(function () { |             $("#apply_config_confirm").click(function () { | ||||||
|                 $.ajax({ |                 $.ajax({ | ||||||
|                     cache: false, |                     cache: false, | ||||||
|                     method: 'GET', |                     method: 'POST', | ||||||
|                     url: '/api/apply-wg-config', |                     url: '{{.basePath}}/api/apply-wg-config', | ||||||
|                     dataType: 'json', |                     dataType: 'json', | ||||||
|                     contentType: "application/json", |                     contentType: "application/json", | ||||||
|                     success: function(data) { |                     success: function(data) { | ||||||
|  |                         updateApplyConfigVisibility() | ||||||
|                         $("#modal_apply_config").modal('hide'); |                         $("#modal_apply_config").modal('hide'); | ||||||
|                         toastr.success('Applied config successfully'); |                         toastr.success('Applied config successfully'); | ||||||
|                     }, |                     }, | ||||||
|  |  | ||||||
|  | @ -68,7 +68,40 @@ Wireguard Clients | ||||||
|                     <span aria-hidden="true">×</span> |                     <span aria-hidden="true">×</span> | ||||||
|                 </button> |                 </button> | ||||||
|             </div> |             </div> | ||||||
|             <img id="qr_code" class="w-100" style="image-rendering: pixelated;" src="" alt="QR code" /> |             <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> |         </div> | ||||||
|         <!-- /.modal-content --> |         <!-- /.modal-content --> | ||||||
|     </div> |     </div> | ||||||
|  | @ -96,6 +129,12 @@ Wireguard Clients | ||||||
|                         <label for="_client_email" class="control-label">Email</label> |                         <label for="_client_email" class="control-label">Email</label> | ||||||
|                         <input type="text" class="form-control" id="_client_email" name="client_email"> |                         <input type="text" class="form-control" id="_client_email" name="client_email"> | ||||||
|                     </div> |                     </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"> |                     <div class="form-group"> | ||||||
|                         <label for="_client_allocated_ips" class="control-label">IP Allocation</label> |                         <label for="_client_allocated_ips" class="control-label">IP Allocation</label> | ||||||
|                         <input type="text" data-role="tagsinput" class="form-control" id="_client_allocated_ips"> |                         <input type="text" data-role="tagsinput" class="form-control" id="_client_allocated_ips"> | ||||||
|  | @ -109,6 +148,10 @@ Wireguard Clients | ||||||
|                         <input type="text" data-role="tagsinput" class="form-control" |                         <input type="text" data-role="tagsinput" class="form-control" | ||||||
|                                id="_client_extra_allowed_ips"> |                                id="_client_extra_allowed_ips"> | ||||||
|                     </div> |                     </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="form-group"> | ||||||
|                         <div class="icheck-primary d-inline"> |                         <div class="icheck-primary d-inline"> | ||||||
|                             <input type="checkbox" id="_use_server_dns"> |                             <input type="checkbox" id="_use_server_dns"> | ||||||
|  | @ -125,6 +168,38 @@ Wireguard Clients | ||||||
|                             </label> |                             </label> | ||||||
|                         </div> |                         </div> | ||||||
|                     </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> | ||||||
|                 <div class="modal-footer justify-content-between"> |                 <div class="modal-footer justify-content-between"> | ||||||
|                     <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> |                     <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> | ||||||
|  | @ -189,7 +264,7 @@ Wireguard Clients | ||||||
|             $.ajax({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'GET', |                 method: 'GET', | ||||||
|                 url: '/api/clients', |                 url: '{{.basePath}}/api/clients', | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 success: function (data) { |                 success: function (data) { | ||||||
|  | @ -207,7 +282,7 @@ Wireguard Clients | ||||||
|             $.ajax({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'POST', |                 method: 'POST', | ||||||
|                 url: '/client/set-status', |                 url: '{{.basePath}}/client/set-status', | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 data: JSON.stringify(data), |                 data: JSON.stringify(data), | ||||||
|  | @ -225,20 +300,219 @@ Wireguard Clients | ||||||
|             setClientStatus(clientID, true); |             setClientStatus(clientID, true); | ||||||
|             const divElement = document.getElementById("paused_" + clientID); |             const divElement = document.getElementById("paused_" + clientID); | ||||||
|             divElement.style.visibility = "hidden"; |             divElement.style.visibility = "hidden"; | ||||||
|  |             updateApplyConfigVisibility() | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         function pauseClient(clientID) { |         function pauseClient(clientID) { | ||||||
|             setClientStatus(clientID, false); |             setClientStatus(clientID, false); | ||||||
|             const divElement = document.getElementById("paused_" + clientID); |             const divElement = document.getElementById("paused_" + clientID); | ||||||
|             divElement.style.visibility = "visible"; |             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> | ||||||
|     <script> |     <script> | ||||||
|         // load client list |         // load client list | ||||||
|         $(document).ready(function () { |         $(document).ready(function () { | ||||||
|  |             updateSearchList(); | ||||||
|             populateClientList(); |             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 modal event | ||||||
|         $("#modal_pause_client").on('show.bs.modal', function (event) { |         $("#modal_pause_client").on('show.bs.modal', function (event) { | ||||||
|             const button = $(event.relatedTarget); |             const button = $(event.relatedTarget); | ||||||
|  | @ -276,7 +550,7 @@ Wireguard Clients | ||||||
|                 $.ajax({ |                 $.ajax({ | ||||||
|                     cache: false, |                     cache: false, | ||||||
|                     method: 'POST', |                     method: 'POST', | ||||||
|                     url: '/remove-client', |                     url: '{{.basePath}}/remove-client', | ||||||
|                     dataType: 'json', |                     dataType: 'json', | ||||||
|                     contentType: "application/json", |                     contentType: "application/json", | ||||||
|                     data: JSON.stringify(data), |                     data: JSON.stringify(data), | ||||||
|  | @ -285,6 +559,7 @@ Wireguard Clients | ||||||
|                         toastr.success('Removed client successfully'); |                         toastr.success('Removed client successfully'); | ||||||
|                         const divElement = document.getElementById('client_' + client_id); |                         const divElement = document.getElementById('client_' + client_id); | ||||||
|                         divElement.style.display = "none"; |                         divElement.style.display = "none"; | ||||||
|  |                         updateApplyConfigVisibility() | ||||||
|                     }, |                     }, | ||||||
|                     error: function(jqXHR, exception) { |                     error: function(jqXHR, exception) { | ||||||
|                         const responseJson = jQuery.parseJSON(jqXHR.responseText); |                         const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||||
|  | @ -297,6 +572,7 @@ Wireguard Clients | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         // Edit client modal event |         // Edit client modal event | ||||||
|  |         // This fills the modal dialogue with data from the DB when we open the edit menu | ||||||
|         $(document).ready(function () { |         $(document).ready(function () { | ||||||
|             $("#modal_edit_client").on('show.bs.modal', function (event) { |             $("#modal_edit_client").on('show.bs.modal', function (event) { | ||||||
|                 let modal = $(this); |                 let modal = $(this); | ||||||
|  | @ -311,6 +587,7 @@ Wireguard Clients | ||||||
|                     'defaultText': 'Add More', |                     'defaultText': 'Add More', | ||||||
|                     'removeWithBackspace': true, |                     'removeWithBackspace': true, | ||||||
|                     'minChars': 0, |                     'minChars': 0, | ||||||
|  |                     'minInputWidth': '100%', | ||||||
|                     'placeholderColor': '#666666' |                     'placeholderColor': '#666666' | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|  | @ -322,6 +599,7 @@ Wireguard Clients | ||||||
|                     'defaultText': 'Add More', |                     'defaultText': 'Add More', | ||||||
|                     'removeWithBackspace': true, |                     'removeWithBackspace': true, | ||||||
|                     'minChars': 0, |                     'minChars': 0, | ||||||
|  |                     'minInputWidth': '100%', | ||||||
|                     'placeholderColor': '#666666' |                     'placeholderColor': '#666666' | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|  | @ -332,6 +610,7 @@ Wireguard Clients | ||||||
|                     'defaultText': 'Add More', |                     'defaultText': 'Add More', | ||||||
|                     'removeWithBackspace' : true, |                     'removeWithBackspace' : true, | ||||||
|                     'minChars': 0, |                     'minChars': 0, | ||||||
|  |                     'minInputWidth': '100%', | ||||||
|                     'placeholderColor': '#666666' |                     'placeholderColor': '#666666' | ||||||
|                 }) |                 }) | ||||||
| 
 | 
 | ||||||
|  | @ -339,7 +618,7 @@ Wireguard Clients | ||||||
|                 $.ajax({ |                 $.ajax({ | ||||||
|                     cache: false, |                     cache: false, | ||||||
|                     method: 'GET', |                     method: 'GET', | ||||||
|                     url: '/api/client/' + client_id, |                     url: '{{.basePath}}/api/client/' + client_id, | ||||||
|                     dataType: 'json', |                     dataType: 'json', | ||||||
|                     contentType: "application/json", |                     contentType: "application/json", | ||||||
|                     success: function (resp) { |                     success: function (resp) { | ||||||
|  | @ -347,9 +626,17 @@ Wireguard Clients | ||||||
| 
 | 
 | ||||||
|                         modal.find(".modal-title").text("Edit Client " + client.name); |                         modal.find(".modal-title").text("Edit Client " + client.name); | ||||||
|                         modal.find("#_client_id").val(client.id); |                         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_name").val(client.name); | ||||||
|                         modal.find("#_client_email").val(client.email); |                         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(''); |                         modal.find("#_client_allocated_ips").importTags(''); | ||||||
|                         client.allocated_ips.forEach(function (obj) { |                         client.allocated_ips.forEach(function (obj) { | ||||||
|                             modal.find("#_client_allocated_ips").addTag(obj); |                             modal.find("#_client_allocated_ips").addTag(obj); | ||||||
|  | @ -365,8 +652,20 @@ Wireguard Clients | ||||||
|                             modal.find("#_client_extra_allowed_ips").addTag(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("#_use_server_dns").prop("checked", client.use_server_dns); | ||||||
|                         modal.find("#_enabled").prop("checked", client.enabled); |                         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) { |                     error: function (jqXHR, exception) { | ||||||
|                         const responseJson = jQuery.parseJSON(jqXHR.responseText); |                         const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||||
|  | @ -376,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() { |         function submitEmailClient() { | ||||||
|             const client_id = $("#e_client_id").val(); |             const client_id = $("#e_client_id").val(); | ||||||
|             const email = $("#e_client_email").val(); |             const email = $("#e_client_email").val(); | ||||||
|  | @ -384,15 +713,36 @@ Wireguard Clients | ||||||
|             $.ajax({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'POST', |                 method: 'POST', | ||||||
|                 url: '/email-client', |                 url: '{{.basePath}}/email-client', | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 data: JSON.stringify(data), |                 data: JSON.stringify(data), | ||||||
|                 success: function(resp) { |                 success: function(resp) { | ||||||
|                     $("#modal_email_client").modal('hide'); |                     $("#modal_email_client").modal('hide'); | ||||||
|                     toastr.success('Sent email to client successfully'); |                     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) { |                 error: function(jqXHR, exception) { | ||||||
|                     const responseJson = jQuery.parseJSON(jqXHR.responseText); |                     const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||||
|  | @ -402,19 +752,26 @@ Wireguard Clients | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // submitEditClient function for updating an existing client |         // 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() { |         function submitEditClient() { | ||||||
|             const client_id = $("#_client_id").val(); |             const client_id = $("#_client_id").val(); | ||||||
|             const name = $("#_client_name").val(); |             const name = $("#_client_name").val(); | ||||||
|             const email = $("#_client_email").val(); |             const email = $("#_client_email").val(); | ||||||
|  |             const telegram_userid = $("#_client_telegram_userid").val(); | ||||||
|             const allocated_ips = $("#_client_allocated_ips").val().split(","); |             const allocated_ips = $("#_client_allocated_ips").val().split(","); | ||||||
|             const allowed_ips = $("#_client_allowed_ips").val().split(","); |             const allowed_ips = $("#_client_allowed_ips").val().split(","); | ||||||
|             let use_server_dns = false; |             let use_server_dns = false; | ||||||
|             let extra_allowed_ips = []; |             let extra_allowed_ips = []; | ||||||
|  |             const public_key = $("#_client_public_key").val(); | ||||||
|  |             const preshared_key = $("#_client_preshared_key").val(); | ||||||
| 
 | 
 | ||||||
|             if( $("#_client_extra_allowed_ips").val() !== "" ) { |             if( $("#_client_extra_allowed_ips").val() !== "" ) { | ||||||
|                 extra_allowed_ips = $("#_client_extra_allowed_ips").val().split(","); |                 extra_allowed_ips = $("#_client_extra_allowed_ips").val().split(","); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             const endpoint = $("#_client_endpoint").val(); | ||||||
|  | 
 | ||||||
|             if ($("#_use_server_dns").is(':checked')){ |             if ($("#_use_server_dns").is(':checked')){ | ||||||
|                 use_server_dns = true; |                 use_server_dns = true; | ||||||
|             } |             } | ||||||
|  | @ -425,13 +782,16 @@ Wireguard Clients | ||||||
|                 enabled = true; |                 enabled = true; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const data = {"id": client_id, "name": name, "email": email, "allocated_ips": allocated_ips, |             const additional_notes = $("#_additional_notes").val(); | ||||||
|                 "allowed_ips": allowed_ips, "extra_allowed_ips": extra_allowed_ips, "use_server_dns": use_server_dns, "enabled": enabled}; | 
 | ||||||
|  |             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({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'POST', |                 method: 'POST', | ||||||
|                 url: '/update-client', |                 url: '{{.basePath}}/update-client', | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 data: JSON.stringify(data), |                 data: JSON.stringify(data), | ||||||
|  | @ -453,8 +813,10 @@ Wireguard Clients | ||||||
|             const formId = $(form).attr('id'); |             const formId = $(form).attr('id'); | ||||||
|             if (formId === "frm_edit_client") { |             if (formId === "frm_edit_client") { | ||||||
|                 submitEditClient(); |                 submitEditClient(); | ||||||
|             }else if (formId === "frm_email_client") { |             } else if (formId === "frm_email_client") { | ||||||
|                 submitEmailClient(); |                 submitEmailClient(); | ||||||
|  |             } else if (formId === "frm_telegram_client") { | ||||||
|  |                 submitTelegramClient(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -465,7 +827,7 @@ Wireguard Clients | ||||||
|             $.ajax({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'GET', |                 method: 'GET', | ||||||
|                 url: '/api/client/' + client_id, |                 url: '{{.basePath}}/api/client/' + client_id, | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 success: function (resp) { |                 success: function (resp) { | ||||||
|  | @ -486,19 +848,27 @@ Wireguard Clients | ||||||
|             let modal = $(this); |             let modal = $(this); | ||||||
|             const button = $(event.relatedTarget); |             const button = $(event.relatedTarget); | ||||||
|             const client_id = button.data('clientid'); |             const client_id = button.data('clientid'); | ||||||
|             const QRCodeImg = modal.find("#qr_code"); | 
 | ||||||
|             QRCodeImg.hide(); |             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({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'GET', |                 method: 'GET', | ||||||
|                 url: '/api/client/' + client_id, |                 url: '{{.basePath}}/api/client/' + client_id, | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 success: function (resp) { |                 success: function (resp) { | ||||||
|                     const client = resp.Client; |                     const client = resp.Client; | ||||||
| 
 | 
 | ||||||
|                     modal.find(".modal-title").text("Scan QR Code for " + client.name + " profile"); |                     modal.find(".modal-title").text("Send config to client " + client.name); | ||||||
|                     QRCodeImg.attr('src', resp.QRCode).show(); |                     modal.find("#tg_client_id").val(client.id); | ||||||
|  |                     modal.find("#tg_client_userid").val(client.telegram_userid); | ||||||
|                 }, |                 }, | ||||||
|                 error: function (jqXHR, exception) { |                 error: function (jqXHR, exception) { | ||||||
|                     const responseJson = jQuery.parseJSON(jqXHR.responseText); |                     const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||||
|  | @ -510,7 +880,6 @@ Wireguard Clients | ||||||
|         $(document).ready(function () { |         $(document).ready(function () { | ||||||
|             $.validator.setDefaults({ |             $.validator.setDefaults({ | ||||||
|                 submitHandler: function (form) { |                 submitHandler: function (form) { | ||||||
|                     //submitEditClient(); |  | ||||||
|                     submitHandler(form); |                     submitHandler(form); | ||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
|  | @ -563,6 +932,33 @@ Wireguard Clients | ||||||
|                     $(element).removeClass('is-invalid'); |                     $(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> |     </script> | ||||||
| {{end}} | {{end}} | ||||||
|  |  | ||||||
|  | @ -55,6 +55,18 @@ Global Settings | ||||||
|                                     name="persistent_keepalive" placeholder="Persistent Keepalive" |                                     name="persistent_keepalive" placeholder="Persistent Keepalive" | ||||||
|                                     value="{{if .globalSettings.PersistentKeepalive }}{{ .globalSettings.PersistentKeepalive }}{{end}}"> |                                     value="{{if .globalSettings.PersistentKeepalive }}{{ .globalSettings.PersistentKeepalive }}{{end}}"> | ||||||
|                             </div> |                             </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"> |                             <div class="form-group"> | ||||||
|                                 <label for="config_file_path">Wireguard Config File Path</label> |                                 <label for="config_file_path">Wireguard Config File Path</label> | ||||||
|                                 <input type="text" class="form-control" id="config_file_path" |                                 <input type="text" class="form-control" id="config_file_path" | ||||||
|  | @ -85,16 +97,20 @@ Global Settings | ||||||
|                             <dt>2. DNS Servers</dt> |                             <dt>2. DNS Servers</dt> | ||||||
|                             <dd>The DNS servers will be set to client config.</dd> |                             <dd>The DNS servers will be set to client config.</dd> | ||||||
|                             <dt>3. MTU</dt> |                             <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> |                                 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> |                             <dt>4. Persistent Keepalive</dt> | ||||||
|                             <dd>By default, WireGuard peers remain silent while they do not need to communicate, |                             <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 |                                 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> |                                 until they reach out to other peers themselves. Adding <code>PersistentKeepalive</code> | ||||||
|                                 can ensure that the connection remains open.</dd> |                                 can ensure that the connection remains open.</dd> | ||||||
|                             <dd>Leave blank to omit this setting in the Client config.</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 |                             <dd>The path of your Wireguard server config file. Please make sure the parent directory | ||||||
|                                 exists and is writable.</dd> |                                 exists and is writable.</dd> | ||||||
|                         </dl> |                         </dl> | ||||||
|  | @ -141,13 +157,15 @@ Global Settings | ||||||
|             const dns_servers = $("#dns_servers").val().split(","); |             const dns_servers = $("#dns_servers").val().split(","); | ||||||
|             const mtu = $("#mtu").val(); |             const mtu = $("#mtu").val(); | ||||||
|             const persistent_keepalive = $("#persistent_keepalive").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 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({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'POST', |                 method: 'POST', | ||||||
|                 url: '/global-settings', |                 url: '{{.basePath}}/global-settings', | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 data: JSON.stringify(data), |                 data: JSON.stringify(data), | ||||||
|  | @ -163,7 +181,7 @@ Global Settings | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         function updateEndpointSuggestionIP() { |         function updateEndpointSuggestionIP() { | ||||||
|             $.getJSON("/api/machine-ips", null, function(data) { |             $.getJSON("{{.basePath}}/api/machine-ips", null, function(data) { | ||||||
|                 $("#ip_suggestion option").remove(); |                 $("#ip_suggestion option").remove(); | ||||||
|                 $.each(data, function(index, item) { |                 $.each(data, function(index, item) { | ||||||
|                     $("#ip_suggestion").append( |                     $("#ip_suggestion").append( | ||||||
|  | @ -185,11 +203,13 @@ Global Settings | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|  |             'minInputWidth': '100%', | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         // Load DNS server to the form |         // Load DNS server to the form | ||||||
|         {{range .globalSettings.DNSServers}} |         {{range .globalSettings.DNSServers}} | ||||||
|  |         $("#dns_servers").removeTag('{{.}}'); | ||||||
|         $("#dns_servers").addTag('{{.}}'); |         $("#dns_servers").addTag('{{.}}'); | ||||||
|         {{end}} |         {{end}} | ||||||
| 
 | 
 | ||||||
|  | @ -211,6 +231,12 @@ Global Settings | ||||||
|                     }, |                     }, | ||||||
|                     config_file_path: { |                     config_file_path: { | ||||||
|                         required: true |                         required: true | ||||||
|  |                     }, | ||||||
|  |                     firewall_mark: { | ||||||
|  |                         required: false | ||||||
|  |                     }, | ||||||
|  |                     table: { | ||||||
|  |                         required: false | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 messages: { |                 messages: { | ||||||
|  |  | ||||||
|  | @ -7,15 +7,17 @@ | ||||||
|     <title>WireGuard UI</title> |     <title>WireGuard UI</title> | ||||||
|     <!-- Tell the browser to be responsive to screen width --> |     <!-- Tell the browser to be responsive to screen width --> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> |     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|  |     <!-- Favicon --> | ||||||
|  |     <link rel="icon" href="{{.basePath}}/favicon"> | ||||||
| 
 | 
 | ||||||
|     <!-- Font Awesome --> |     <!-- 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 --> |     <!-- Ionicons --> | ||||||
|     <link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"> |     <link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"> | ||||||
|     <!-- icheck bootstrap --> |     <!-- 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 --> |     <!-- 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 --> |     <!-- Google Font: Source Sans Pro --> | ||||||
|     <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet"> |     <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet"> | ||||||
| </head> | </head> | ||||||
|  | @ -71,21 +73,21 @@ | ||||||
|     </div> |     </div> | ||||||
|     <!-- /.login-box --> |     <!-- /.login-box --> | ||||||
|     <!-- jQuery --> |     <!-- jQuery --> | ||||||
|     <script src="static/plugins/jquery/jquery.min.js"></script> |     <script src="{{.basePath}}/static/plugins/jquery/jquery.min.js"></script> | ||||||
|     <!-- Bootstrap 4 --> |     <!-- 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 --> |     <!-- AdminLTE App --> | ||||||
|     <script src="static/dist/js/adminlte.min.js"></script> |     <script src="{{.basePath}}/static/dist/js/adminlte.min.js"></script> | ||||||
| 
 | 
 | ||||||
| </body> | </body> | ||||||
| <script> | <script> | ||||||
|     function redirectNext() { |     function redirectNext() { | ||||||
|         const urlParams = new URLSearchParams(window.location.search); |         const urlParams = new URLSearchParams(window.location.search); | ||||||
|         const nextURL = urlParams.get('next'); |         const nextURL = urlParams.get('next'); | ||||||
|         if (nextURL) { |         if (nextURL && /(?:^\/[a-zA-Z_])|(?:^\/$)/.test(nextURL.trim())) { | ||||||
|             window.location.href = nextURL; |             window.location.href = nextURL; | ||||||
|         } else { |         } else { | ||||||
|             window.location.href = '/'; |             window.location.href = '/{{.basePath}}'; | ||||||
|         }  |         }  | ||||||
|     } |     } | ||||||
| </script> | </script> | ||||||
|  | @ -99,13 +101,16 @@ | ||||||
|         $("#btn_login").click(function () { |         $("#btn_login").click(function () { | ||||||
|             const username = $("#username").val(); |             const username = $("#username").val(); | ||||||
|             const password = $("#password").val(); |             const password = $("#password").val(); | ||||||
|             const data = {"username": username, "password": password} |             let rememberMe = false; | ||||||
|             console.log(data); |             if ($("#remember").is(':checked')){ | ||||||
|  |                 rememberMe = true; | ||||||
|  |             } | ||||||
|  |             const data = {"username": username, "password": password, "rememberMe": rememberMe} | ||||||
| 
 | 
 | ||||||
|             $.ajax({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'POST', |                 method: 'POST', | ||||||
|                 url: '/login', |                 url: '{{.basePath}}/login', | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 data: JSON.stringify(data), |                 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" |                                 <input type="text" class="form-control" id="post_up" name="post_up" | ||||||
|                                        placeholder="Post Up Script" value="{{ .serverInterface.PostUp }}"> |                                        placeholder="Post Up Script" value="{{ .serverInterface.PostUp }}"> | ||||||
|                             </div> |                             </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"> |                             <div class="form-group"> | ||||||
|                                 <label for="post_down">Post Down Script</label> |                                 <label for="post_down">Post Down Script</label> | ||||||
|                                 <input type="text" class="form-control" id="post_down" name="post_down" |                                 <input type="text" class="form-control" id="post_down" name="post_down" | ||||||
|  | @ -110,7 +116,7 @@ Wireguard Server Settings | ||||||
|             </div> |             </div> | ||||||
|             <div class="modal-body"> |             <div class="modal-body"> | ||||||
|                 <p>Are you sure to generate a new key pair for the Wireguard server?<br/> |                 <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> | ||||||
|             <div class="modal-footer justify-content-between"> |             <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" data-dismiss="modal">Cancel</button> | ||||||
|  | @ -130,13 +136,14 @@ Wireguard Server Settings | ||||||
|             const addresses = $("#addresses").val().split(","); |             const addresses = $("#addresses").val().split(","); | ||||||
|             const listen_port = $("#listen_port").val(); |             const listen_port = $("#listen_port").val(); | ||||||
|             const post_up = $("#post_up").val(); |             const post_up = $("#post_up").val(); | ||||||
|  |             const pre_down = $("#pre_down").val(); | ||||||
|             const post_down = $("#post_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({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'POST', |                 method: 'POST', | ||||||
|                 url: '/wg-server/interfaces', |                 url: '{{.basePath}}/wg-server/interfaces', | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 data: JSON.stringify(data), |                 data: JSON.stringify(data), | ||||||
|  | @ -160,11 +167,13 @@ Wireguard Server Settings | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|  |             'minInputWidth': '100%', | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         // Load server addresses to the form |         // Load server addresses to the form | ||||||
|         {{range .serverInterface.Addresses}} |         {{range .serverInterface.Addresses}} | ||||||
|  |         $("#addresses").removeTag('{{.}}'); | ||||||
|         $("#addresses").addTag('{{.}}'); |         $("#addresses").addTag('{{.}}'); | ||||||
|         {{end}} |         {{end}} | ||||||
| 
 | 
 | ||||||
|  | @ -210,7 +219,7 @@ Wireguard Server Settings | ||||||
|                 $.ajax({ |                 $.ajax({ | ||||||
|                     cache: false, |                     cache: false, | ||||||
|                     method: 'POST', |                     method: 'POST', | ||||||
|                     url: '/wg-server/keypair', |                     url: '{{.basePath}}/wg-server/keypair', | ||||||
|                     dataType: 'json', |                     dataType: 'json', | ||||||
|                     contentType: "application/json", |                     contentType: "application/json", | ||||||
|                     success: function(data) { |                     success: function(data) { | ||||||
|  |  | ||||||
|  | @ -25,7 +25,7 @@ Connected Peers | ||||||
|       if (pow == units.length-1) break |       if (pow == units.length-1) break | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return parseFloat(temporal.toFixed(3)) + units[pow]+"Bytes" |     return parseFloat(temporal.toFixed(2)) + units[pow]+"B" | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
| <section class="content"> | <section class="content"> | ||||||
|  | @ -41,11 +41,13 @@ Connected Peers | ||||||
|                   <th scope="col">#</th> |                   <th scope="col">#</th> | ||||||
|                   <th scope="col">Name</th> |                   <th scope="col">Name</th> | ||||||
|                   <th scope="col">Email</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">Public Key</th> | ||||||
|                   <th scope="col">ReceiveBytes</th> |                   <th scope="col">Received</th> | ||||||
|                   <th scope="col">TransmitBytes</th> |                   <th scope="col">Transmitted</th> | ||||||
|                   <th scope="col">Connected (Approximation)</th> |                   <th scope="col">Connected (Approximation)</th> | ||||||
|                   <th scope="col">LastHandshakeTime</th> |                   <th scope="col">Last Handshake</th> | ||||||
|                 </tr> |                 </tr> | ||||||
|               </thead> |               </thead> | ||||||
|               <tbody> |               <tbody> | ||||||
|  | @ -54,11 +56,13 @@ Connected Peers | ||||||
|                 <th scope="row">{{ $idx }}</th> |                 <th scope="row">{{ $idx }}</th> | ||||||
|                 <td>{{ $peer.Name }}</td> |                 <td>{{ $peer.Name }}</td> | ||||||
|                 <td>{{ $peer.Email }}</td> |                 <td>{{ $peer.Email }}</td> | ||||||
|  |                 <td>{{ $peer.AllocatedIP }}</td> | ||||||
|  |                 <td>{{ $peer.Endpoint }}</td> | ||||||
|                 <td>{{ $peer.PublicKey }}</td> |                 <td>{{ $peer.PublicKey }}</td> | ||||||
|                 <td title="{{ $peer.ReceivedBytes }} Bytes"><script>document.write(bytesToHumanReadable({{ $peer.ReceivedBytes }}))</script></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 title="{{ $peer.TransmitBytes }} Bytes"><script>document.write(bytesToHumanReadable({{ $peer.TransmitBytes }}))</script></td> | ||||||
|                 <td>{{ $peer.Connected }}</td> |                 <td>{{ if $peer.Connected }}✓{{end}}</td> | ||||||
|                 <td>{{ $peer.LastHandshakeTime }}</td> |                 <td>{{ $peer.LastHandshakeTime.Format "2006-01-02 15:04:05 MST" }}</td> | ||||||
|                </tr> |                </tr> | ||||||
|               {{ end }} |               {{ end }} | ||||||
|               </tbody> |               </tbody> | ||||||
|  |  | ||||||
|  | @ -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}} | ||||||
|  | @ -103,7 +103,7 @@ | ||||||
|                                 <span class="info-box-text"><i class="fas fa-clock"></i> |                                 <span class="info-box-text"><i class="fas fa-clock"></i> | ||||||
|                                     <span class="latest-used"> |                                     <span class="latest-used"> | ||||||
|                                         {{ if .LatestUsed }} |                                         {{ if .LatestUsed }} | ||||||
|                                             {{ .LatestUsed }} |                                             {{ .LatestUsed.Format "2006-01-02T15:04:05Z07:00"}} | ||||||
|                                         {{ else }} |                                         {{ else }} | ||||||
|                                             Unused |                                             Unused | ||||||
|                                         {{ end }} |                                         {{ end }} | ||||||
|  | @ -119,5 +119,5 @@ | ||||||
| 
 | 
 | ||||||
| {{end}} | {{end}} | ||||||
| {{define "bottom_js"}} | {{define "bottom_js"}} | ||||||
|     <script src="/static/custom/js/wake_on_lan_hosts.js"></script> |     <script src="{{.basePath}}/static/custom/js/wake_on_lan_hosts.js"></script> | ||||||
| {{end}} | {{end}} | ||||||
|  | @ -9,16 +9,25 @@ ListenPort = {{ .serverConfig.Interface.ListenPort }} | ||||||
| PrivateKey = {{ .serverConfig.KeyPair.PrivateKey }} | PrivateKey = {{ .serverConfig.KeyPair.PrivateKey }} | ||||||
| {{if .globalSettings.MTU}}MTU = {{ .globalSettings.MTU }}{{end}} | {{if .globalSettings.MTU}}MTU = {{ .globalSettings.MTU }}{{end}} | ||||||
| PostUp = {{ .serverConfig.Interface.PostUp }} | PostUp = {{ .serverConfig.Interface.PostUp }} | ||||||
|  | PreDown = {{ .serverConfig.Interface.PreDown }} | ||||||
| PostDown = {{ .serverConfig.Interface.PostDown }} | PostDown = {{ .serverConfig.Interface.PostDown }} | ||||||
|  | Table = {{ .globalSettings.Table }} | ||||||
| 
 | 
 | ||||||
| {{range .clientDataList}}{{if eq .Client.Enabled true}} | {{range .clientDataList}}{{if eq .Client.Enabled true}} | ||||||
| # ID:           {{ .Client.ID }} | # ID:           {{ .Client.ID }} | ||||||
| # Name:         {{ .Client.Name }} | # Name:         {{ .Client.Name }} | ||||||
| # Email:        {{ .Client.Email }} | # Email:        {{ .Client.Email }} | ||||||
|  | # Telegram:     {{ .Client.TgUserid }} | ||||||
| # Created at:   {{ .Client.CreatedAt }} | # Created at:   {{ .Client.CreatedAt }} | ||||||
| # Update at:    {{ .Client.UpdatedAt }} | # Update at:    {{ .Client.UpdatedAt }} | ||||||
|  | {{- if .Client.AdditionalNotes}} | ||||||
|  | 
 | ||||||
|  | # Notes: | ||||||
|  | # {{ .Client.AdditionalNotes }}{{end}} | ||||||
| [Peer] | [Peer] | ||||||
| PublicKey = {{ .Client.PublicKey }} | PublicKey = {{ .Client.PublicKey }} | ||||||
| {{if .Client.PresharedKey }}PresharedKey = {{ .Client.PresharedKey }} | {{if .Client.PresharedKey}}PresharedKey = {{ .Client.PresharedKey }}{{end}} | ||||||
| {{end}}AllowedIPs = {{$first :=true}}{{range .Client.AllocatedIPs }}{{if $first}}{{$first = false}}{{else}},{{end}}{{.}}{{end}}{{range .Client.ExtraAllowedIPs }},{{.}}{{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}} | {{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{} | ||||||
							
								
								
									
										135
									
								
								util/config.go
								
								
								
								
							
							
						
						
									
										135
									
								
								util/config.go
								
								
								
								
							|  | @ -1,32 +1,119 @@ | ||||||
| package util | package util | ||||||
| 
 | 
 | ||||||
|  | import ( | ||||||
|  | 	"net" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"github.com/labstack/gommon/log" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| // Runtime config
 | // Runtime config
 | ||||||
| var ( | var ( | ||||||
| 	DisableLogin   bool | 	DisableLogin       bool | ||||||
| 	BindAddress    string | 	BindAddress        string | ||||||
| 	SmtpHostname   string | 	SmtpHostname       string | ||||||
| 	SmtpPort       int | 	SmtpPort           int | ||||||
| 	SmtpUsername   string | 	SmtpUsername       string | ||||||
| 	SmtpPassword   string | 	SmtpPassword       string | ||||||
| 	SmtpNoTLSCheck bool | 	SmtpNoTLSCheck     bool | ||||||
| 	SmtpAuthType   string | 	SmtpEncryption     string | ||||||
| 	SendgridApiKey string | 	SmtpAuthType       string | ||||||
| 	EmailFrom      string | 	SmtpHelo           string | ||||||
| 	EmailFromName  string | 	SendgridApiKey     string | ||||||
| 	EmailSubject   string | 	EmailFrom          string | ||||||
| 	EmailContent   string | 	EmailFromName      string | ||||||
| 	SessionSecret  []byte | 	SessionSecret      [64]byte | ||||||
|  | 	SessionMaxDuration int64 | ||||||
|  | 	WgConfTemplate     string | ||||||
|  | 	BasePath           string | ||||||
|  | 	SubnetRanges       map[string]([]*net.IPNet) | ||||||
|  | 	SubnetRangesOrder  []string | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	DefaultUsername            = "admin" | 	DefaultUsername                        = "admin" | ||||||
| 	DefaultPassword            = "admin" | 	DefaultPassword                        = "admin" | ||||||
| 	DefaultServerAddress       = "10.252.1.0/24" | 	DefaultIsAdmin                         = true | ||||||
| 	DefaultServerPort          = 51820 | 	DefaultServerAddress                   = "10.252.1.0/24" | ||||||
| 	DefaultDNS                 = "1.1.1.1" | 	DefaultServerPort                      = 51820 | ||||||
| 	DefaultMTU                 = 1450 | 	DefaultDNS                             = "1.1.1.1" | ||||||
| 	DefaultPersistentKeepalive = 15 | 	DefaultMTU                             = 1450 | ||||||
| 	DefaultConfigFilePath      = "/etc/wireguard/wg0.conf" | 	DefaultPersistentKeepalive             = 15 | ||||||
| 	UsernameEnvVar             = "WGUI_USERNAME" | 	DefaultFirewallMark                    = "0xca6c" // i.e. 51820
 | ||||||
| 	PasswordEnvVar             = "WGUI_PASSWORD" | 	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 | ||||||
|  | } | ||||||
							
								
								
									
										496
									
								
								util/util.go
								
								
								
								
							
							
						
						
									
										496
									
								
								util/util.go
								
								
								
								
							|  | @ -1,23 +1,42 @@ | ||||||
| package util | package util | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/gob" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"hash/crc32" | ||||||
|  | 	"io" | ||||||
|  | 	"io/fs" | ||||||
|  | 	"math/rand" | ||||||
| 	"net" | 	"net" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path" | ||||||
|  | 	"path/filepath" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"text/template" | 	"text/template" | ||||||
| 	"time" | 	"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" | 	externalip "github.com/glendc/go-external-ip" | ||||||
| 	"github.com/labstack/gommon/log" | 	"github.com/labstack/gommon/log" | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/model" | 	"github.com/ngoduykhanh/wireguard-ui/model" | ||||||
| 	"github.com/sdomino/scribble" | 	"github.com/sdomino/scribble" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | var qrCodeSettings = model.QRCodeSettings{ | ||||||
|  | 	Enabled:    true, | ||||||
|  | 	IncludeDNS: true, | ||||||
|  | 	IncludeMTU: true, | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // BuildClientConfig to create wireguard client config string
 | // BuildClientConfig to create wireguard client config string
 | ||||||
| func BuildClientConfig(client model.Client, server model.Server, setting model.GlobalSetting) string { | func BuildClientConfig(client model.Client, server model.Server, setting model.GlobalSetting) string { | ||||||
| 	// Interface section
 | 	// Interface section
 | ||||||
|  | @ -27,6 +46,10 @@ func BuildClientConfig(client model.Client, server model.Server, setting model.G | ||||||
| 	if client.UseServerDNS { | 	if client.UseServerDNS { | ||||||
| 		clientDNS = fmt.Sprintf("DNS = %s\n", 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
 | 	// Peer section
 | ||||||
| 	peerPublicKey := fmt.Sprintf("PublicKey = %s\n", server.KeyPair.PublicKey) | 	peerPublicKey := fmt.Sprintf("PublicKey = %s\n", server.KeyPair.PublicKey) | ||||||
|  | @ -60,6 +83,7 @@ func BuildClientConfig(client model.Client, server model.Server, setting model.G | ||||||
| 		clientAddress + | 		clientAddress + | ||||||
| 		clientPrivateKey + | 		clientPrivateKey + | ||||||
| 		clientDNS + | 		clientDNS + | ||||||
|  | 		clientMTU + | ||||||
| 		"\n[Peer]\n" + | 		"\n[Peer]\n" + | ||||||
| 		peerPublicKey + | 		peerPublicKey + | ||||||
| 		peerPresharedKey + | 		peerPresharedKey + | ||||||
|  | @ -70,6 +94,26 @@ func BuildClientConfig(client model.Client, server model.Server, setting model.G | ||||||
| 	return strConfig | 	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
 | // ValidateCIDR to validate a network CIDR
 | ||||||
| func ValidateCIDR(cidr string) bool { | func ValidateCIDR(cidr string) bool { | ||||||
| 	_, _, err := net.ParseCIDR(cidr) | 	_, _, err := net.ParseCIDR(cidr) | ||||||
|  | @ -147,7 +191,7 @@ func GetInterfaceIPs() ([]model.Interface, error) { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var interfaceList = []model.Interface{} | 	var interfaceList []model.Interface | ||||||
| 
 | 
 | ||||||
| 	// get interface's ip addresses
 | 	// get interface's ip addresses
 | ||||||
| 	for _, i := range ifaces { | 	for _, i := range ifaces { | ||||||
|  | @ -188,9 +232,9 @@ func GetPublicIP() (model.Interface, error) { | ||||||
| 	consensus := externalip.NewConsensus(&cfg, nil) | 	consensus := externalip.NewConsensus(&cfg, nil) | ||||||
| 
 | 
 | ||||||
| 	// add trusted voters
 | 	// 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://whatismyip.akamai.com"), 1) | ||||||
| 	consensus.AddVoter(externalip.NewHTTPSource("http://ifconfig.top"), 1) | 	consensus.AddVoter(externalip.NewHTTPSource("https://ifconfig.top"), 1) | ||||||
| 
 | 
 | ||||||
| 	publicInterface := model.Interface{} | 	publicInterface := model.Interface{} | ||||||
| 	publicInterface.Name = "Public Address" | 	publicInterface.Name = "Public Address" | ||||||
|  | @ -198,10 +242,12 @@ func GetPublicIP() (model.Interface, error) { | ||||||
| 	ip, err := consensus.ExternalIP() | 	ip, err := consensus.ExternalIP() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		publicInterface.IPAddress = "N/A" | 		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
 | // GetIPFromCIDR get ip from CIDR
 | ||||||
|  | @ -248,7 +294,7 @@ func GetAllocatedIPs(ignoreClientID string) ([]string, error) { | ||||||
| 	// append client's addresses to the result
 | 	// append client's addresses to the result
 | ||||||
| 	for _, f := range records { | 	for _, f := range records { | ||||||
| 		client := model.Client{} | 		client := model.Client{} | ||||||
| 		if err := json.Unmarshal([]byte(f), &client); err != nil { | 		if err := json.Unmarshal(f, &client); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -290,17 +336,34 @@ func GetBroadcastIP(n *net.IPNet) net.IP { | ||||||
| 	return broadcast | 	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
 | // GetAvailableIP get the ip address that can be allocated from an CIDR
 | ||||||
| func GetAvailableIP(cidr string, allocatedList []string) (string, error) { | // We need interfaceAddresses to find real broadcast and network addresses
 | ||||||
| 	ip, net, err := net.ParseCIDR(cidr) | func GetAvailableIP(cidr string, allocatedList, interfaceAddresses []string) (string, error) { | ||||||
|  | 	ip, netAddr, err := net.ParseCIDR(cidr) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	broadcastAddr := GetBroadcastIP(net).String() | 	unavailableIPs := GetBroadcastAndNetworkAddrsLookup(interfaceAddresses) | ||||||
| 	networkAddr := net.IP.String() |  | ||||||
| 
 | 
 | ||||||
| 	for ip := ip.Mask(net.Mask); net.Contains(ip); inc(ip) { | 	for ip := ip.Mask(netAddr.Mask); netAddr.Contains(ip); inc(ip) { | ||||||
| 		available := true | 		available := true | ||||||
| 		suggestedAddr := ip.String() | 		suggestedAddr := ip.String() | ||||||
| 		for _, allocatedAddr := range allocatedList { | 		for _, allocatedAddr := range allocatedList { | ||||||
|  | @ -309,7 +372,7 @@ func GetAvailableIP(cidr string, allocatedList []string) (string, error) { | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		if available && suggestedAddr != networkAddr && suggestedAddr != broadcastAddr { | 		if available && !unavailableIPs[suggestedAddr] { | ||||||
| 			return suggestedAddr, nil | 			return suggestedAddr, nil | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -325,7 +388,7 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip | ||||||
| 
 | 
 | ||||||
| 		// clientCIDR must be in CIDR format
 | 		// clientCIDR must be in CIDR format
 | ||||||
| 		if ip == nil { | 		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)
 | 		// return false immediately if the ip is already in use (in ipAllocatedList)
 | ||||||
|  | @ -337,7 +400,7 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip | ||||||
| 
 | 
 | ||||||
| 		// even if it is not in use, we still need to check if it
 | 		// even if it is not in use, we still need to check if it
 | ||||||
| 		// belongs to a network of the server.
 | 		// belongs to a network of the server.
 | ||||||
| 		var isValid bool = false | 		var isValid = false | ||||||
| 		for _, serverCIDR := range serverAddresses { | 		for _, serverCIDR := range serverAddresses { | ||||||
| 			_, serverNet, _ := net.ParseCIDR(serverCIDR) | 			_, serverNet, _ := net.ParseCIDR(serverCIDR) | ||||||
| 			if serverNet.Contains(ip) { | 			if serverNet.Contains(ip) { | ||||||
|  | @ -357,13 +420,154 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip | ||||||
| 	return true, nil | 	return true, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
 | // findSubnetRangeForIP to find first SR for IP, and cache the match
 | ||||||
| func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, clientDataList []model.ClientData, globalSettings model.GlobalSetting) error { | func findSubnetRangeForIP(cidr string) (uint16, error) { | ||||||
| 	// read wg.conf template file to string
 | 	ip, _, err := net.ParseCIDR(cidr) | ||||||
| 	tmplWireguardConf, err := tmplBox.String("wg.conf") | 	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 { | 	if err != nil { | ||||||
| 		return err | 		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
 | 	// parse the template
 | ||||||
| 	t, err := template.New("wg_config").Parse(tmplWireguardConf) | 	t, err := template.New("wg_config").Parse(tmplWireguardConf) | ||||||
|  | @ -379,8 +583,9 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl | ||||||
| 
 | 
 | ||||||
| 	config := map[string]interface{}{ | 	config := map[string]interface{}{ | ||||||
| 		"serverConfig":   serverConfig, | 		"serverConfig":   serverConfig, | ||||||
| 		"clientDataList": clientDataList, | 		"clientDataList": escapedClientDataList, | ||||||
| 		"globalSettings": globalSettings, | 		"globalSettings": globalSettings, | ||||||
|  | 		"usersList":      usersList, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err = t.Execute(f, config) | 	err = t.Execute(f, config) | ||||||
|  | @ -392,6 +597,57 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl | ||||||
| 	return nil | 	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 { | func LookupEnvOrString(key string, defaultVal string) string { | ||||||
| 	if val, ok := os.LookupEnv(key); ok { | 	if val, ok := os.LookupEnv(key); ok { | ||||||
| 		return val | 		return val | ||||||
|  | @ -421,10 +677,200 @@ func LookupEnvOrInt(key string, defaultVal int) int { | ||||||
| 	return defaultVal | 	return defaultVal | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetCredVar reads value from environment variable or returns fallback
 | func LookupEnvOrStrings(key string, defaultVal []string) []string { | ||||||
| func GetCredVar(key, fallback string) string { | 	if val, ok := os.LookupEnv(key); ok { | ||||||
| 	if value, ok := os.LookupEnv(key); ok { | 		return strings.Split(val, ",") | ||||||
| 		return value |  | ||||||
| 	} | 	} | ||||||
| 	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