Compare commits
	
		
			No commits in common. "master" and "v0.5.0" have entirely different histories.
		
	
	
		|  | @ -1 +0,0 @@ | ||||||
| *.html linguist-detectable=false |  | ||||||
|  | @ -11,7 +11,7 @@ jobs: | ||||||
|   build-image: |   build-image: | ||||||
|     runs-on: ubuntu-22.04 |     runs-on: ubuntu-22.04 | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v3 | ||||||
| 
 | 
 | ||||||
|       # set environment |       # set environment | ||||||
|       - name: Set BUILD_TIME env |       - name: Set BUILD_TIME env | ||||||
|  | @ -33,13 +33,14 @@ jobs: | ||||||
|           ## |           ## | ||||||
|           if [[ '${{ github.ref }}' == *"refs/tags/"* ]]; then |           if [[ '${{ github.ref }}' == *"refs/tags/"* ]]; then | ||||||
|             github_tag="${GITHUB_REF#refs/*/}" |             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-]+)*)?$" |             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 |             if [[ "$github_tag" =~ $SEMVER_REGEX ]]; then | ||||||
|               github_tag=$(echo "${github_tag}" | sed 's/^v//') |               github_tag=$(echo "${github_tag}" | sed 's/^v//') | ||||||
|             fi |             fi | ||||||
| 
 | 
 | ||||||
|  |             app_version=${github_tag} | ||||||
|  | 
 | ||||||
|             container_images=$(cat <<END_HEREDOC |             container_images=$(cat <<END_HEREDOC | ||||||
|           ${base}:${github_tag} |           ${base}:${github_tag} | ||||||
|           END_HEREDOC |           END_HEREDOC | ||||||
|  | @ -71,19 +72,19 @@ jobs: | ||||||
| 
 | 
 | ||||||
|       # set up docker and build images |       # set up docker and build images | ||||||
|       - name: Set up QEMU |       - name: Set up QEMU | ||||||
|         uses: docker/setup-qemu-action@v3 |         uses: docker/setup-qemu-action@v2 | ||||||
| 
 | 
 | ||||||
|       - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|         uses: docker/setup-buildx-action@v3 |         uses: docker/setup-buildx-action@v2 | ||||||
| 
 | 
 | ||||||
|       - name: Login to Docker Hub |       - name: Login to Docker Hub | ||||||
|         uses: docker/login-action@v3 |         uses: docker/login-action@v2 | ||||||
|         with: |         with: | ||||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} |           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} |           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||||
| 
 | 
 | ||||||
|       - name: Build and push |       - name: Build and push | ||||||
|         uses: docker/build-push-action@v5 |         uses: docker/build-push-action@v4 | ||||||
|         with: |         with: | ||||||
|           push: true |           push: true | ||||||
|           context: . |           context: . | ||||||
|  |  | ||||||
|  | @ -1,31 +0,0 @@ | ||||||
| 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 |  | ||||||
|  | @ -9,7 +9,6 @@ jobs: | ||||||
|     name: Release Go Binary |     name: Release Go Binary | ||||||
|     runs-on: ubuntu-22.04 |     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] | ||||||
|  | @ -25,7 +24,7 @@ jobs: | ||||||
|           - 7 |           - 7 | ||||||
|     steps: |     steps: | ||||||
|     # get the source code |     # get the source code | ||||||
|     - uses: actions/checkout@v4 |     - uses: actions/checkout@v3 | ||||||
| 
 | 
 | ||||||
|     # set environment |     # set environment | ||||||
|     - name: Set APP_VERSION env |     - name: Set APP_VERSION env | ||||||
|  | @ -36,9 +35,9 @@ jobs: | ||||||
|       uses: managedkaos/print-env@v1.0 |       uses: managedkaos/print-env@v1.0 | ||||||
| 
 | 
 | ||||||
|     # setup node |     # setup node | ||||||
|     - uses: actions/setup-node@v4 |     - uses: actions/setup-node@v2 | ||||||
|       with: |       with: | ||||||
|         node-version: '20' |         node-version: '14' | ||||||
|         registry-url: 'https://registry.npmjs.org' |         registry-url: 'https://registry.npmjs.org' | ||||||
| 
 | 
 | ||||||
|     # prepare assets |     # prepare assets | ||||||
|  | @ -54,7 +53,7 @@ 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.21.5.linux-amd64.tar.gz" |         goversion: "https://dl.google.com/go/go1.16.1.linux-amd64.tar.gz" | ||||||
|         pre_command: export CGO_ENABLED=0 |         pre_command: export CGO_ENABLED=0 | ||||||
|         binary_name: "wireguard-ui" |         binary_name: "wireguard-ui" | ||||||
|         build_flags: -v |         build_flags: -v | ||||||
|  |  | ||||||
|  | @ -14,17 +14,13 @@ 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/ | ||||||
| 
 | 
 | ||||||
| # IDEs | # IDEs | ||||||
| .vscode | .vscode | ||||||
| .idea | .idea | ||||||
| 
 | 
 | ||||||
| # Vim |  | ||||||
| .*.sw[op] |  | ||||||
| 
 |  | ||||||
| # Examples | # Examples | ||||||
| examples/docker-compose/config | examples/docker-compose/config | ||||||
| examples/docker-compose/db | examples/docker-compose/db | ||||||
|  |  | ||||||
|  | @ -1,26 +0,0 @@ | ||||||
| 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 |  | ||||||
|  | @ -1,67 +0,0 @@ | ||||||
| # 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. |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # Build stage | # Build stage | ||||||
| FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.21-alpine3.19 AS builder | FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.17-alpine3.16 as builder | ||||||
| LABEL maintainer="Khanh Ngo <k@ndk.name>" | LABEL maintainer="Khanh Ngo <k@ndk.name>" | ||||||
| 
 | 
 | ||||||
| ARG BUILDPLATFORM | ARG BUILDPLATFORM | ||||||
|  | @ -56,12 +56,12 @@ RUN cp -r /build/custom/ assets/ | ||||||
| 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 . | RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X 'main.appVersion=${APP_VERSION}' -X 'main.buildTime=${BUILD_TIME}' -X 'main.gitCommit=${GIT_COMMIT}'" -a -o wg-ui . | ||||||
| 
 | 
 | ||||||
| # Release stage | # Release stage | ||||||
| FROM alpine:3.19 | FROM alpine:3.16 | ||||||
| 
 | 
 | ||||||
| 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 wireguard-tools jq iptables | RUN apk --no-cache add ca-certificates wireguard-tools jq | ||||||
| 
 | 
 | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
| 
 | 
 | ||||||
|  | @ -71,7 +71,7 @@ RUN mkdir -p db | ||||||
| COPY --from=builder --chown=wgui:wgui /build/wg-ui . | COPY --from=builder --chown=wgui:wgui /build/wg-ui . | ||||||
| RUN chmod +x wg-ui | RUN chmod +x wg-ui | ||||||
| COPY init.sh . | 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:$(echo ${BIND_ADDRESS:-5000} | cut -d ':' -f2)/_health"] | ||||||
| ENTRYPOINT ["./init.sh"] | ENTRYPOINT ["./init.sh"] | ||||||
|  |  | ||||||
							
								
								
									
										77
									
								
								README.md
								
								
								
								
							
							
						
						
									
										77
									
								
								README.md
								
								
								
								
							|  | @ -8,8 +8,8 @@ A web user interface to manage your WireGuard setup. | ||||||
| 
 | 
 | ||||||
| - Friendly UI | - Friendly UI | ||||||
| - Authentication | - Authentication | ||||||
| - Manage extra client information (name, email, etc.) | - Manage extra client information (name, email, etc) | ||||||
| - Retrieve client config using QR code / file / email / Telegram | - Retrieve client config using QR code / file / email | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
| 
 | 
 | ||||||
|  | @ -36,44 +36,33 @@ docker-compose up | ||||||
| 
 | 
 | ||||||
| ## Environment Variables | ## Environment Variables | ||||||
| 
 | 
 | ||||||
| | Variable                      | Description                                                                                                                                                                                                                                                                         | Default                            | | | 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                                | | | `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`              | The secret key used to encrypt the session cookies. Set this to a random value                                                                                                                                                                                                      | N/A                                | | | `WGUI_USERNAME`             | The username for the login page. Used for db initialization only                                                                                             | `admin`                            | | ||||||
| | `SESSION_SECRET_FILE`         | Optional filepath for the secret key used to encrypt the session cookies. Leave `SESSION_SECRET` blank to take effect                                                                                                                                                               | N/A                                | | | `WGUI_PASSWORD`             | The password for the user on the login page. Will be hashed automatically. Used for db initialization only                                                   | `admin`                            | | ||||||
| | `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                                 | | | `WGUI_PASSWORD_HASH`        | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only                                          | N/A                                | | ||||||
| | `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_ENDPOINT_ADDRESS`     | The default endpoint address used in global settings where clients should connect to                                                                         | Resolved to your public ip address | | ||||||
| | `WGUI_USERNAME`               | The username for the login page. Used for db initialization only                                                                                                                                                                                                                    | `admin`                            | | | `WGUI_FAVICON_FILE_PATH`    | The file path used as website favicon                                                                                                                        | Embedded WireGuard logo            | | ||||||
| | `WGUI_PASSWORD`               | The password for the user on the login page. Will be hashed automatically. Used for db initialization only                                                                                                                                                                          | `admin`                            | | | `WGUI_ENDPOINT_ADDRESS`     | The default endpoint address used in global settings                                                                                                         | Resolved to your public ip address | | ||||||
| | `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_DNS`                  | The default DNS servers (comma-separated-list) used in the global settings                                                                                   | `1.1.1.1`                          | | ||||||
| | `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_MTU`                  | The default MTU used in global settings                                                                                                                      | `1450`                             | | ||||||
| | `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_PERSISTENT_KEEPALIVE` | The default persistent keepalive for WireGuard in global settings                                                                                            | `15`                               | | ||||||
| | `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_FIREWALL_MARK`        | The default WireGuard firewall mark                                                                                                                          | `0xca6c`  (51820)                  | | ||||||
| | `WGUI_FAVICON_FILE_PATH`      | The file path used as website favicon                                                                                                                                                                                                                                               | Embedded WireGuard logo            | | | `WGUI_TABLE`                | The default WireGuard table value settings                                                                                                                   | `auto`                             | | ||||||
| | `WGUI_DNS`                    | The default DNS servers (comma-separated-list) used in the global settings                                                                                                                                                                                                          | `1.1.1.1`                          | | | `WGUI_CONFIG_FILE_PATH`     | The default WireGuard config file path used in global settings                                                                                               | `/etc/wireguard/wg0.conf`          | | ||||||
| | `WGUI_MTU`                    | The default MTU used in global settings                                                                                                                                                                                                                                             | `1450`                             | | | `WGUI_LOG_LEVEL`            | The default log level. Possible values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF`                                                                              | `INFO`                             | | ||||||
| | `WGUI_PERSISTENT_KEEPALIVE`   | The default persistent keepalive for WireGuard in global settings                                                                                                                                                                                                                   | `15`                               | | | `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                                | | ||||||
| | `WGUI_FIREWALL_MARK`          | The default WireGuard firewall mark                                                                                                                                                                                                                                                 | `0xca6c`  (51820)                  | | | `EMAIL_FROM_ADDRESS`        | The sender email address                                                                                                                                     | N/A                                | | ||||||
| | `WGUI_TABLE`                  | The default WireGuard table value settings                                                                                                                                                                                                                                          | `auto`                             | | | `EMAIL_FROM_NAME`           | The sender name                                                                                                                                              | `WireGuard UI`                     | | ||||||
| | `WGUI_CONFIG_FILE_PATH`       | The default WireGuard config file path used in global settings                                                                                                                                                                                                                      | `/etc/wireguard/wg0.conf`          | | | `SENDGRID_API_KEY`          | The SendGrid api key                                                                                                                                         | N/A                                | | ||||||
| | `WGUI_LOG_LEVEL`              | The default log level. Possible values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF`                                                                                                                                                                                                     | `INFO`                             | | | `SMTP_HOSTNAME`             | The SMTP IP address or hostname                                                                                                                              | `127.0.0.1`                        | | ||||||
| | `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                                | | | `SMTP_PORT`                 | The SMTP port                                                                                                                                                | `25`                               | | ||||||
| | `EMAIL_FROM_ADDRESS`          | The sender email address                                                                                                                                                                                                                                                            | N/A                                | | | `SMTP_USERNAME`             | The SMTP username                                                                                                                                            | N/A                                | | ||||||
| | `EMAIL_FROM_NAME`             | The sender name                                                                                                                                                                                                                                                                     | `WireGuard UI`                     | | | `SMTP_PASSWORD`             | The SMTP user password                                                                                                                                       | N/A                                | | ||||||
| | `SENDGRID_API_KEY`            | The SendGrid api key                                                                                                                                                                                                                                                                | N/A                                | | | `SMTP_AUTH_TYPE`            | The SMTP authentication type. Possible values: `PLAIN`, `LOGIN`, `NONE`                                                                                      | `NONE`                             | | ||||||
| | `SENDGRID_API_KEY_FILE`       | Optional filepath for the SendGrid api key. Leave `SENDGRID_API_KEY` blank to take effect                                                                                                                                                                                           | N/A                                | | | `SMTP_ENCRYPTION`           | the encryption method. Possible values: `NONE`, `SSL`, `SSLTLS`, `TLS`, `STARTTLS`                                                                           | `STARTTLS`                         | | ||||||
| | `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 | ### Defaults for server configuration | ||||||
| 
 | 
 | ||||||
|  | @ -196,7 +185,8 @@ 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 | 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 | 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 | Path, after restarting the container. Please make sure you have `--cap-add=NET_ADMIN` in your container config to make | ||||||
| this feature work. | this | ||||||
|  | feature work. | ||||||
| 
 | 
 | ||||||
| ## Build | ## Build | ||||||
| 
 | 
 | ||||||
|  | @ -214,9 +204,7 @@ or | ||||||
| docker compose build --build-arg=GIT_COMMIT=$(git rev-parse --short HEAD) | 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) | :information_source: A container image is avaialble on [Docker Hub](https://hub.docker.com/r/ngoduykhanh/wireguard-ui) which you can pull and use  | ||||||
| which you can pull and use |  | ||||||
| 
 |  | ||||||
| ``` | ``` | ||||||
| docker pull ngoduykhanh/wireguard-ui | docker pull ngoduykhanh/wireguard-ui | ||||||
| ```` | ```` | ||||||
|  | @ -230,7 +218,6 @@ Prepare the assets directory | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Then build your executable | Then build your executable | ||||||
| 
 |  | ||||||
| ```sh | ```sh | ||||||
| go build -o wireguard-ui | go build -o wireguard-ui | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | @ -1,20 +1,5 @@ | ||||||
| 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) { | ||||||
|  | @ -33,23 +18,13 @@ 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" style="overflow: hidden"> |                             <div class="info-box-content"> | ||||||
|                                 <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> | ||||||
|  | @ -63,7 +38,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"  | ||||||
|  | @ -84,9 +59,6 @@ function renderClientList(data) { | ||||||
|                                 <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-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> | ||||||
|  | @ -94,8 +66,6 @@ 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>` | ||||||
|  |  | ||||||
|  | @ -3,10 +3,9 @@ package emailer | ||||||
| import ( | import ( | ||||||
| 	"crypto/tls" | 	"crypto/tls" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	mail "github.com/xhit/go-simple-mail/v2" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 |  | ||||||
| 	mail "github.com/xhit/go-simple-mail/v2" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type SmtpMail struct { | type SmtpMail struct { | ||||||
|  | @ -14,7 +13,6 @@ 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 | 	encryption mail.Encryption | ||||||
| 	noTLSCheck bool | 	noTLSCheck bool | ||||||
|  | @ -48,8 +46,8 @@ func encryptionType(encryptionType string) mail.Encryption { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewSmtpMail(hostname string, port int, username string, password string, SmtpHelo string, noTLSCheck bool, auth string, fromName, from string, encryption string) *SmtpMail { | func NewSmtpMail(hostname string, port int, username string, password 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)} | 	ans := SmtpMail{hostname: hostname, port: port, username: username, password: password, noTLSCheck: noTLSCheck, fromName: fromName, from: from, authType: authType(auth), encryption: encryptionType(encryption)} | ||||||
| 	return &ans | 	return &ans | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -68,7 +66,6 @@ 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.Helo = o.smtpHelo |  | ||||||
| 	server.Encryption = o.encryption | 	server.Encryption = o.encryption | ||||||
| 	server.KeepAlive = false | 	server.KeepAlive = false | ||||||
| 	server.ConnectTimeout = 10 * time.Second | 	server.ConnectTimeout = 10 * time.Second | ||||||
|  |  | ||||||
							
								
								
									
										61
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										61
									
								
								go.mod
								
								
								
								
							|  | @ -1,52 +1,27 @@ | ||||||
| module github.com/ngoduykhanh/wireguard-ui | module github.com/ngoduykhanh/wireguard-ui | ||||||
| 
 | 
 | ||||||
| go 1.21 | go 1.16 | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
| 	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/leodido/go-urn v1.2.0 // indirect | ||||||
|  | 	github.com/rs/xid v1.2.1 | ||||||
| 	github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d | 	github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d | ||||||
| 	github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8 | 	github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba | ||||||
| 	github.com/sendgrid/sendgrid-go v3.14.0+incompatible | 	github.com/sendgrid/rest v2.6.4+incompatible // indirect | ||||||
| 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e | 	github.com/sendgrid/sendgrid-go v3.10.0+incompatible | ||||||
| 	github.com/xhit/go-simple-mail/v2 v2.16.0 | 	github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 | ||||||
| 	golang.org/x/crypto v0.17.0 | 	github.com/xhit/go-simple-mail/v2 v2.10.0 | ||||||
| 	golang.org/x/mod v0.14.0 | 	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 | ||||||
|  | 	golang.org/x/mod v0.7.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 | ||||||
| ) | ) | ||||||
| 
 |  | ||||||
| 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 |  | ||||||
| ) |  | ||||||
|  |  | ||||||
							
								
								
									
										237
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										237
									
								
								go.sum
								
								
								
								
							|  | @ -1,68 +1,90 @@ | ||||||
| github.com/NicoNex/echotron/v3 v3.27.0 h1:iq4BLPO+Dz1JHjh2HPk0D0NldAZSYcAjaOicgYEhUzw= | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= | ||||||
| github.com/NicoNex/echotron/v3 v3.27.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI= | 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/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 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/glendc/go-external-ip v0.1.0 h1:iX3xQ2Q26atAmLTbd++nUce2P5ht5P4uD4V7caSY/xg= | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= | ||||||
| github.com/glendc/go-external-ip v0.1.0/go.mod h1:CNx312s2FLAJoWNdJWZ2Fpf5O4oLsMFwuYviHjS4uJE= | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | ||||||
| github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd h1:1BzxHapafGJd/XlpMvocLeDBin2EKn90gXv2AQt5sfo= | ||||||
| github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd/go.mod h1:o9OoDQyE1WHvYVUH1FdFapy1/rCZHHq3O5wS4VA83ig= | ||||||
| github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | ||||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= | ||||||
| github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= | ||||||
| github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= | ||||||
| github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= | ||||||
| github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= | ||||||
|  | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= | ||||||
|  | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= | ||||||
|  | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | ||||||
|  | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||||
|  | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||||
|  | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||||
| github.com/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/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
| github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= | ||||||
| github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= | ||||||
| github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= | ||||||
| github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | ||||||
| github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= | github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= | ||||||
| github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= | github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= | ||||||
| github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= | github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | ||||||
| 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/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= | ||||||
| github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw= | github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw= | ||||||
| github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs= | github.com/jsimonetti/rtnetlink v0.0.0-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 h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do= | ||||||
| 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/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU= | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= | ||||||
| github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4= | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||||
| github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= | ||||||
| github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | ||||||
| github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= | ||||||
| github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= | github.com/labstack/echo-contrib v0.9.0 h1:hKBA2SnxdxR7sghH0J04zq/pImnKRmgvmQ6MvY9hug4= | ||||||
| github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= | github.com/labstack/echo-contrib v0.9.0/go.mod h1:TsFE5Vv0LRpZLoh4mMmaaAxzcTH+1CBFiUtVhwlegzU= | ||||||
| github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= | github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE= | ||||||
|  | github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o= | ||||||
|  | github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI= | ||||||
|  | github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4= | ||||||
|  | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= | ||||||
|  | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= | ||||||
|  | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= | ||||||
|  | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= | ||||||
|  | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= | ||||||
|  | github.com/mattn/go-colorable v0.1.6/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-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= | ||||||
| github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= | ||||||
| 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/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= | ||||||
| github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY= | ||||||
| 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= | ||||||
|  | @ -71,58 +93,86 @@ 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 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/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/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= | ||||||
|  | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
|  | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| 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/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= | ||||||
| github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= | ||||||
|  | github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= | ||||||
|  | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= | ||||||
|  | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||||
|  | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= | ||||||
|  | github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= | ||||||
|  | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= | ||||||
|  | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= | ||||||
|  | github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= | ||||||
|  | github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= | ||||||
|  | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= | ||||||
| github.com/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-20230717151034-b95d4df19aa8 h1:hlNRl87eAZhh2QMJVShuXHL6OOd0ObZM0JozDIruNeM= | github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba h1:8QAc9wFAf2b/9cAXskm0wBylObZ0bTpRcaP7ThjLPVQ= | ||||||
| github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8/go.mod h1:W6zxGUBCXRR5QugSd/nFcFVmwoGnvpjiNY/JwT03Wew= | github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba/go.mod h1:W6zxGUBCXRR5QugSd/nFcFVmwoGnvpjiNY/JwT03Wew= | ||||||
| github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= | github.com/sendgrid/rest v2.6.4+incompatible h1:lq6gAQxLwVBf3mVyCCSHI6mgF+NfaJFJHjT0kl6SSo8= | ||||||
| github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= | github.com/sendgrid/rest v2.6.4+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= | ||||||
| github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA= | github.com/sendgrid/sendgrid-go v3.10.0+incompatible h1:aSYyurHxEZSDy7kxhvZ4fH0inNkEEmRssZNbAmETR2c= | ||||||
| github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= | github.com/sendgrid/sendgrid-go v3.10.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= | ||||||
| github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= | ||||||
| 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 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= | ||||||
|  | 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= | ||||||
| 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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||||
| github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | ||||||
| github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||||
| github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= | ||||||
| github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM= | github.com/uber/jaeger-client-go v2.19.1-0.20191002155754-0be28c34dabf+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= | ||||||
| github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= | github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= | ||||||
| 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.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= | ||||||
| github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= | github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= | ||||||
| github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA= | github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= | ||||||
| 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 h1:nib6RaJ4qVh5HD9UE9QJqnUZyWp3upv+Z6CFxaMj0V8= | ||||||
|  | github.com/xhit/go-simple-mail/v2 v2.10.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4= | ||||||
|  | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||||||
|  | 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/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= | golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= | ||||||
| golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= | ||||||
| golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||||
| golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||||||
| golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= | ||||||
|  | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||||
|  | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | golang.org/x/net v0.0.0-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
| golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | golang.org/x/net v0.0.0-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= | ||||||
|  | @ -130,19 +180,29 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v | ||||||
| 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/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||||
| golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= | golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= | ||||||
| golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||||
| golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
|  | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
| golang.org/x/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= | ||||||
|  | @ -155,34 +215,41 @@ golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7w | ||||||
| golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.0.0-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/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.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= | ||||||
| golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| 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/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||||
| 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/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.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= | ||||||
| golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||||
| 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/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||||
|  | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||||||
|  | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
|  | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= | ||||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.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 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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= | ||||||
|  | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||||
|  |  | ||||||
|  | @ -1,9 +1,8 @@ | ||||||
| package handler | package handler | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"net/http" |  | ||||||
| 
 |  | ||||||
| 	"github.com/labstack/echo/v4" | 	"github.com/labstack/echo/v4" | ||||||
|  | 	"net/http" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // ContentTypeJson checks that the requests have the Content-Type header set to "application/json".
 | // ContentTypeJson checks that the requests have the Content-Type header set to "application/json".
 | ||||||
|  |  | ||||||
|  | @ -8,9 +8,7 @@ import ( | ||||||
| 	"io/fs" | 	"io/fs" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
| 	"regexp" |  | ||||||
| 	"sort" | 	"sort" | ||||||
| 	"strconv" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | @ -19,19 +17,15 @@ import ( | ||||||
| 	"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 { | ||||||
|  | @ -69,14 +63,9 @@ func Login(db store.IStore) echo.HandlerFunc { | ||||||
| 		password := data["password"].(string) | 		password := data["password"].(string) | ||||||
| 		rememberMe := data["rememberMe"].(bool) | 		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) | 		dbuser, err := db.GetUserByName(username) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Infof("Cannot query user %s from DB", username) | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot query user from DB"}) | ||||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Invalid credentials"}) |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		userCorrect := subtle.ConstantTimeCompare([]byte(username), []byte(dbuser.Username)) == 1 | 		userCorrect := subtle.ConstantTimeCompare([]byte(username), []byte(dbuser.Username)) == 1 | ||||||
|  | @ -93,41 +82,32 @@ func Login(db store.IStore) echo.HandlerFunc { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if userCorrect && passwordCorrect { | 		if userCorrect && passwordCorrect { | ||||||
|  | 			// TODO: refresh the token
 | ||||||
| 			ageMax := 0 | 			ageMax := 0 | ||||||
|  | 			expiration := time.Now().Add(24 * time.Hour) | ||||||
| 			if rememberMe { | 			if rememberMe { | ||||||
| 				ageMax = 86400 * 7 | 				ageMax = 86400 | ||||||
|  | 				expiration.Add(144 * time.Hour) | ||||||
| 			} | 			} | ||||||
| 
 |  | ||||||
| 			cookiePath := util.GetCookiePath() |  | ||||||
| 
 |  | ||||||
| 			sess, _ := session.Get("session", c) | 			sess, _ := session.Get("session", c) | ||||||
| 			sess.Options = &sessions.Options{ | 			sess.Options = &sessions.Options{ | ||||||
| 				Path:     cookiePath, | 				Path:     util.BasePath, | ||||||
| 				MaxAge:   ageMax, | 				MaxAge:   ageMax, | ||||||
| 				HttpOnly: true, | 				HttpOnly: true, | ||||||
| 				SameSite: http.SameSiteLaxMode, |  | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// set session_token
 | 			// set session_token
 | ||||||
| 			tokenUID := xid.New().String() | 			tokenUID := xid.New().String() | ||||||
| 			now := time.Now().UTC().Unix() |  | ||||||
| 			sess.Values["username"] = dbuser.Username | 			sess.Values["username"] = dbuser.Username | ||||||
| 			sess.Values["user_hash"] = util.GetDBUserCRC32(dbuser) |  | ||||||
| 			sess.Values["admin"] = dbuser.Admin | 			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.MaxAge = ageMax | 			cookie.Expires = expiration | ||||||
| 			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"}) | ||||||
|  | @ -140,6 +120,7 @@ func Login(db store.IStore) echo.HandlerFunc { | ||||||
| // GetUsers handler return a JSON list of all users
 | // GetUsers handler return a JSON list of all users
 | ||||||
| func GetUsers(db store.IStore) echo.HandlerFunc { | func GetUsers(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
|  | 
 | ||||||
| 		usersList, err := db.GetUsers() | 		usersList, err := db.GetUsers() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | ||||||
|  | @ -154,11 +135,8 @@ func GetUsers(db store.IStore) echo.HandlerFunc { | ||||||
| // GetUser handler returns a JSON object of single user
 | // GetUser handler returns a JSON object of single user
 | ||||||
| func GetUser(db store.IStore) echo.HandlerFunc { | func GetUser(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 		username := c.Param("username") |  | ||||||
| 
 | 
 | ||||||
| 		if !usernameRegexp.MatchString(username) { | 		username := c.Param("username") | ||||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		if !isAdmin(c) && (username != currentUser(c)) { | 		if !isAdmin(c) && (username != currentUser(c)) { | ||||||
| 			return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"}) | 			return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"}) | ||||||
|  | @ -182,7 +160,7 @@ func Logout() echo.HandlerFunc { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // LoadProfile to load user information
 | // LoadProfile to load user information
 | ||||||
| func LoadProfile() echo.HandlerFunc { | func LoadProfile(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 		return c.Render(http.StatusOK, "profile.html", map[string]interface{}{ | 		return c.Render(http.StatusOK, "profile.html", map[string]interface{}{ | ||||||
| 			"baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | 			"baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
|  | @ -191,7 +169,7 @@ func LoadProfile() echo.HandlerFunc { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // UsersSettings handler
 | // UsersSettings handler
 | ||||||
| func UsersSettings() echo.HandlerFunc { | func UsersSettings(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 		return c.Render(http.StatusOK, "users_settings.html", map[string]interface{}{ | 		return c.Render(http.StatusOK, "users_settings.html", map[string]interface{}{ | ||||||
| 			"baseData": model.BaseData{Active: "users-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | 			"baseData": model.BaseData{Active: "users-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
|  | @ -222,16 +200,12 @@ func UpdateUser(db store.IStore) echo.HandlerFunc { | ||||||
| 			admin = false | 			admin = false | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if !usernameRegexp.MatchString(previousUsername) { |  | ||||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		user, err := db.GetUserByName(previousUsername) | 		user, err := db.GetUserByName(previousUsername) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) | 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if username == "" || !usernameRegexp.MatchString(username) { | 		if username == "" { | ||||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) | ||||||
| 		} else { | 		} else { | ||||||
| 			user.Username = username | 			user.Username = username | ||||||
|  | @ -265,7 +239,7 @@ func UpdateUser(db store.IStore) echo.HandlerFunc { | ||||||
| 		log.Infof("Updated user information successfully") | 		log.Infof("Updated user information successfully") | ||||||
| 
 | 
 | ||||||
| 		if previousUsername == currentUser(c) { | 		if previousUsername == currentUser(c) { | ||||||
| 			setUser(c, user.Username, user.Admin, util.GetDBUserCRC32(user)) | 			setUser(c, user.Username, user.Admin) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated user information successfully"}) | 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated user information successfully"}) | ||||||
|  | @ -287,7 +261,7 @@ func CreateUser(db store.IStore) echo.HandlerFunc { | ||||||
| 		password := data["password"].(string) | 		password := data["password"].(string) | ||||||
| 		admin := data["admin"].(bool) | 		admin := data["admin"].(bool) | ||||||
| 
 | 
 | ||||||
| 		if username == "" || !usernameRegexp.MatchString(username) { | 		if username == "" { | ||||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) | 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) | ||||||
| 		} else { | 		} else { | ||||||
| 			user.Username = username | 			user.Username = username | ||||||
|  | @ -329,10 +303,6 @@ func RemoveUser(db store.IStore) echo.HandlerFunc { | ||||||
| 
 | 
 | ||||||
| 		username := data["username"].(string) | 		username := data["username"].(string) | ||||||
| 
 | 
 | ||||||
| 		if !usernameRegexp.MatchString(username) { |  | ||||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if username == currentUser(c) { | 		if username == currentUser(c) { | ||||||
| 			return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "User cannot delete itself"}) | 			return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "User cannot delete itself"}) | ||||||
| 		} | 		} | ||||||
|  | @ -352,6 +322,7 @@ func RemoveUser(db store.IStore) echo.HandlerFunc { | ||||||
| // 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{ | ||||||
|  | @ -369,6 +340,7 @@ func WireGuardClients(db store.IStore) echo.HandlerFunc { | ||||||
| // GetClients handler return a JSON 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{ | ||||||
|  | @ -376,10 +348,6 @@ 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) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -387,16 +355,12 @@ func GetClients(db store.IStore) echo.HandlerFunc { | ||||||
| // GetClient handler returns a JSON object 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") | ||||||
| 
 |  | ||||||
| 		if _, err := xid.FromString(clientID); err != nil { |  | ||||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"}) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		qrCodeSettings := model.QRCodeSettings{ | 		qrCodeSettings := model.QRCodeSettings{ | ||||||
| 			Enabled:    true, | 			Enabled:       true, | ||||||
| 			IncludeDNS: true, | 			IncludeDNS:    true, | ||||||
| 			IncludeMTU: true, | 			IncludeMTU:    true, | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		clientData, err := db.GetClientByID(clientID, qrCodeSettings) | 		clientData, err := db.GetClientByID(clientID, qrCodeSettings) | ||||||
|  | @ -404,24 +368,17 @@ func GetClient(db store.IStore) echo.HandlerFunc { | ||||||
| 			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, util.FillClientSubnetRange(clientData)) | 		return c.JSON(http.StatusOK, 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 { | ||||||
|  | @ -479,6 +436,7 @@ 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 == "" { | ||||||
|  | @ -527,14 +485,10 @@ func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailCon | ||||||
| 		c.Bind(&payload) | 		c.Bind(&payload) | ||||||
| 		// TODO validate email
 | 		// TODO validate email
 | ||||||
| 
 | 
 | ||||||
| 		if _, err := xid.FromString(payload.ID); err != nil { |  | ||||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"}) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		qrCodeSettings := model.QRCodeSettings{ | 		qrCodeSettings := model.QRCodeSettings{ | ||||||
| 			Enabled:    true, | 			Enabled:       true, | ||||||
| 			IncludeDNS: true, | 			IncludeDNS:    true, | ||||||
| 			IncludeMTU: true, | 			IncludeMTU:    true, | ||||||
| 		} | 		} | ||||||
| 		clientData, err := db.GetClientByID(payload.ID, qrCodeSettings) | 		clientData, err := db.GetClientByID(payload.ID, qrCodeSettings) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -547,14 +501,14 @@ 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) | ||||||
| 
 | 
 | ||||||
| 		cfgAtt := emailer.Attachment{Name: "wg0.conf", Data: []byte(config)} | 		cfgAtt := emailer.Attachment{"wg0.conf", []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()}) | ||||||
| 			} | 			} | ||||||
| 			qrAtt := emailer.Attachment{Name: "wg.png", Data: qrdata} | 			qrAtt := emailer.Attachment{"wg.png", qrdata} | ||||||
| 			attachments = []emailer.Attachment{cfgAtt, qrAtt} | 			attachments = []emailer.Attachment{cfgAtt, qrAtt} | ||||||
| 		} else { | 		} else { | ||||||
| 			attachments = []emailer.Attachment{cfgAtt} | 			attachments = []emailer.Attachment{cfgAtt} | ||||||
|  | @ -575,75 +529,19 @@ 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, model.QRCodeSettings{Enabled: 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{ | ||||||
|  | @ -669,58 +567,15 @@ 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 { | ||||||
|  | @ -735,6 +590,7 @@ 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) | ||||||
| 
 | 
 | ||||||
|  | @ -745,10 +601,6 @@ func SetClientStatus(db store.IStore) echo.HandlerFunc { | ||||||
| 		clientID := data["id"].(string) | 		clientID := data["id"].(string) | ||||||
| 		status := data["status"].(bool) | 		status := data["status"].(bool) | ||||||
| 
 | 
 | ||||||
| 		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}) | 		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()}) | ||||||
|  | @ -774,10 +626,6 @@ 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"}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		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}) | 		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) | ||||||
|  | @ -800,20 +648,17 @@ 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/conf", reader) | 		return c.Stream(http.StatusOK, "text/plain", 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 { | ||||||
|  | @ -829,6 +674,7 @@ 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) | ||||||
|  | @ -845,6 +691,7 @@ 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) | ||||||
| 
 | 
 | ||||||
|  | @ -870,6 +717,7 @@ 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 { | ||||||
|  | @ -894,6 +742,7 @@ 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) | ||||||
|  | @ -926,6 +775,7 @@ 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{}{ | ||||||
|  | @ -979,13 +829,10 @@ func Status(db store.IStore) echo.HandlerFunc { | ||||||
| 						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, | 						AllocatedIP:       allocatedIPs, | ||||||
|  | 						Endpoint:          devices[i].Peers[j].Endpoint.String(), | ||||||
| 					} | 					} | ||||||
| 					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 | ||||||
|  | @ -1009,6 +856,7 @@ 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) | ||||||
| 
 | 
 | ||||||
|  | @ -1034,6 +882,7 @@ 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 { | ||||||
|  | @ -1054,16 +903,10 @@ 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) | ||||||
|  | @ -1081,48 +924,22 @@ 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 { | ||||||
| 		sr := c.QueryParam("sr") | 			ip, err := util.GetAvailableIP(cidr, allocatedIPs) | ||||||
| 		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) | ||||||
| 				continue | 				return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | ||||||
|  | 					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, ":") { | ||||||
| 				ipSet[fmt.Sprintf("%s/128", ip)] = struct{}{} | 				suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/128", ip)) | ||||||
| 			} else { | 			} else { | ||||||
| 				ipSet[fmt.Sprintf("%s/32", ip)] = struct{}{} | 				suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/32", ip)) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		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) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -1130,6 +947,7 @@ func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { | ||||||
| // 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, tmplDir fs.FS) 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) | ||||||
|  | @ -1175,6 +993,7 @@ func ApplyServerConfig(db store.IStore, tmplDir fs.FS) echo.HandlerFunc { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| // GetHashesChanges handler returns if database hashes have changed
 | // GetHashesChanges handler returns if database hashes have changed
 | ||||||
| func GetHashesChanges(db store.IStore) echo.HandlerFunc { | func GetHashesChanges(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
|  |  | ||||||
|  | @ -2,15 +2,14 @@ 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 { | ||||||
|  |  | ||||||
|  | @ -3,9 +3,7 @@ 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" | ||||||
|  | @ -25,15 +23,6 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 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 { | func NeedsAdmin(next echo.HandlerFunc) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 		if !isAdmin(c) { | 		if !isAdmin(c) { | ||||||
|  | @ -52,146 +41,9 @@ 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 { | ||||||
|  | @ -214,10 +66,9 @@ func isAdmin(c echo.Context) bool { | ||||||
| 	return admin == "true" | 	return admin == "true" | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func setUser(c echo.Context, username string, admin bool, userCRC32 uint32) { | func setUser(c echo.Context, username string, admin bool) { | ||||||
| 	sess, _ := session.Get("session", c) | 	sess, _ := session.Get("session", c) | ||||||
| 	sess.Values["username"] = username | 	sess.Values["username"] = username | ||||||
| 	sess.Values["user_hash"] = userCRC32 |  | ||||||
| 	sess.Values["admin"] = admin | 	sess.Values["admin"] = admin | ||||||
| 	sess.Save(c.Request(), c.Response()) | 	sess.Save(c.Request(), c.Response()) | ||||||
| } | } | ||||||
|  | @ -226,24 +77,7 @@ func setUser(c echo.Context, username string, admin bool, userCRC32 uint32) { | ||||||
| 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["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) |  | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										192
									
								
								main.go
								
								
								
								
							
							
						
						
									
										192
									
								
								main.go
								
								
								
								
							|  | @ -1,22 +1,16 @@ | ||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"crypto/sha512" |  | ||||||
| 	"embed" | 	"embed" | ||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/fs" |  | ||||||
| 	"net" |  | ||||||
| 	"net/http" |  | ||||||
| 	"os" |  | ||||||
| 	"strings" |  | ||||||
| 	"syscall" |  | ||||||
| 	"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/store" | 	"github.com/ngoduykhanh/wireguard-ui/store" | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/telegram" | 	"io/fs" | ||||||
|  | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/emailer" | 	"github.com/ngoduykhanh/wireguard-ui/emailer" | ||||||
| 	"github.com/ngoduykhanh/wireguard-ui/handler" | 	"github.com/ngoduykhanh/wireguard-ui/handler" | ||||||
|  | @ -32,27 +26,21 @@ 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             = false | 	flagDisableLogin   bool   = false | ||||||
| 	flagBindAddress              = "0.0.0.0:5000" | 	flagBindAddress    string = "0.0.0.0:5000" | ||||||
| 	flagSmtpHostname             = "127.0.0.1" | 	flagSmtpHostname   string = "127.0.0.1" | ||||||
| 	flagSmtpPort                 = 25 | 	flagSmtpPort       int    = 25 | ||||||
| 	flagSmtpUsername             string | 	flagSmtpUsername   string | ||||||
| 	flagSmtpPassword             string | 	flagSmtpPassword   string | ||||||
| 	flagSmtpAuthType             = "NONE" | 	flagSmtpAuthType   string = "NONE" | ||||||
| 	flagSmtpNoTLSCheck           = false | 	flagSmtpNoTLSCheck bool   = false | ||||||
| 	flagSmtpEncryption           = "STARTTLS" | 	flagSmtpEncryption string = "STARTTLS" | ||||||
| 	flagSmtpHelo                 = "localhost" | 	flagSendgridApiKey string | ||||||
| 	flagSendgridApiKey           string | 	flagEmailFrom      string | ||||||
| 	flagEmailFrom                string | 	flagEmailFromName  string = "WireGuard UI" | ||||||
| 	flagEmailFromName            = "WireGuard UI" | 	flagSessionSecret  string | ||||||
| 	flagTelegramToken            string | 	flagWgConfTemplate string | ||||||
| 	flagTelegramAllowConfRequest = false | 	flagBasePath       string | ||||||
| 	flagTelegramFloodWait        = 60 |  | ||||||
| 	flagSessionSecret            = util.RandomString(32) |  | ||||||
| 	flagSessionMaxDuration       = 90 |  | ||||||
| 	flagWgConfTemplate           string |  | ||||||
| 	flagBasePath                 string |  | ||||||
| 	flagSubnetRanges             string |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
|  | @ -75,53 +63,23 @@ var embeddedTemplates embed.FS | ||||||
| var embeddedAssets embed.FS | var embeddedAssets embed.FS | ||||||
| 
 | 
 | ||||||
| func init() { | 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(&flagSmtpHelo, "smtp-helo", util.LookupEnvOrString("SMTP_HELO", flagSmtpHelo), "SMTP HELO Hostname") |  | ||||||
| 	flag.StringVar(&flagSmtpUsername, "smtp-username", util.LookupEnvOrString("SMTP_USERNAME", flagSmtpUsername), "SMTP Username") | 	flag.StringVar(&flagSmtpUsername, "smtp-username", util.LookupEnvOrString("SMTP_USERNAME", flagSmtpUsername), "SMTP Username") | ||||||
|  | 	flag.StringVar(&flagSmtpPassword, "smtp-password", util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword), "SMTP Password") | ||||||
| 	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(&flagSmtpEncryption, "smtp-encryption", util.LookupEnvOrString("SMTP_ENCRYPTION", flagSmtpEncryption), "SMTP Encryption : NONE, SSL, SSLTLS, TLS or STARTTLS (by default)") | 	flag.StringVar(&flagSmtpEncryption, "smtp-encryption", util.LookupEnvOrString("SMTP_ENCRYPTION", flagSmtpEncryption), "SMTP Encryption : NONE, SSL, SSLTLS, TLS or STARTTLS (by default)") | ||||||
| 	flag.StringVar(&flagSmtpAuthType, "smtp-auth-type", util.LookupEnvOrString("SMTP_AUTH_TYPE", flagSmtpAuthType), "SMTP Auth Type : PLAIN, LOGIN or NONE.") | 	flag.StringVar(&flagSmtpAuthType, "smtp-auth-type", util.LookupEnvOrString("SMTP_AUTH_TYPE", flagSmtpAuthType), "SMTP Auth Type : PLAIN, LOGIN or NONE.") | ||||||
|  | 	flag.StringVar(&flagSendgridApiKey, "sendgrid-api-key", util.LookupEnvOrString("SENDGRID_API_KEY", flagSendgridApiKey), "Your sendgrid api key.") | ||||||
| 	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(&flagTelegramToken, "telegram-token", util.LookupEnvOrString("TELEGRAM_TOKEN", flagTelegramToken), "Telegram bot token for distributing configs to clients.") | 	flag.StringVar(&flagSessionSecret, "session-secret", util.LookupEnvOrString("SESSION_SECRET", flagSessionSecret), "The key used to encrypt session cookies.") | ||||||
| 	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(&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(&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
 | ||||||
|  | @ -129,7 +87,6 @@ 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 | ||||||
|  | @ -138,21 +95,12 @@ func init() { | ||||||
| 	util.SendgridApiKey = flagSendgridApiKey | 	util.SendgridApiKey = flagSendgridApiKey | ||||||
| 	util.EmailFrom = flagEmailFrom | 	util.EmailFrom = flagEmailFrom | ||||||
| 	util.EmailFromName = flagEmailFromName | 	util.EmailFromName = flagEmailFromName | ||||||
| 	util.SessionSecret = sha512.Sum512([]byte(flagSessionSecret)) | 	util.SessionSecret = []byte(flagSessionSecret) | ||||||
| 	util.SessionMaxDuration = int64(flagSessionMaxDuration) * 86_400 // Store in seconds
 |  | ||||||
| 	util.WgConfTemplate = flagWgConfTemplate | 	util.WgConfTemplate = flagWgConfTemplate | ||||||
| 	util.BasePath = util.ParseBasePath(flagBasePath) | 	util.BasePath = util.ParseBasePath(flagBasePath) | ||||||
| 	util.SubnetRanges = util.ParseSubnetRanges(flagSubnetRanges) |  | ||||||
| 
 |  | ||||||
| 	lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")) |  | ||||||
| 
 |  | ||||||
| 	telegram.Token = flagTelegramToken |  | ||||||
| 	telegram.AllowConfRequest = flagTelegramAllowConfRequest |  | ||||||
| 	telegram.FloodWait = flagTelegramFloodWait |  | ||||||
| 	telegram.LogLevel = lvl |  | ||||||
| 
 | 
 | ||||||
| 	// print only if log level is INFO or lower
 | 	// print only if log level is INFO or lower
 | ||||||
| 	if lvl <= log.INFO { | 	if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO { | ||||||
| 		// print app information
 | 		// print app information
 | ||||||
| 		fmt.Println("Wireguard UI") | 		fmt.Println("Wireguard UI") | ||||||
| 		fmt.Println("App Version\t:", appVersion) | 		fmt.Println("App Version\t:", appVersion) | ||||||
|  | @ -168,7 +116,6 @@ func init() { | ||||||
| 		//fmt.Println("Session secret\t:", util.SessionSecret)
 | 		//fmt.Println("Session secret\t:", util.SessionSecret)
 | ||||||
| 		fmt.Println("Custom wg.conf\t:", util.WgConfTemplate) | 		fmt.Println("Custom wg.conf\t:", util.WgConfTemplate) | ||||||
| 		fmt.Println("Base path\t:", util.BasePath+"/") | 		fmt.Println("Base path\t:", util.BasePath+"/") | ||||||
| 		fmt.Println("Subnet ranges\t:", util.GetSubnetRangesString()) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -194,36 +141,21 @@ func main() { | ||||||
| 	// create the wireguard config on start, if it doesn't exist
 | 	// create the wireguard config on start, if it doesn't exist
 | ||||||
| 	initServerConfig(db, tmplDir) | 	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(tmplDir, extraData, util.SessionSecret) | 	app := router.New(tmplDir, extraData, util.SessionSecret) | ||||||
| 
 | 
 | ||||||
| 	app.GET(util.BasePath, handler.WireGuardClients(db), handler.ValidSession, handler.RefreshSession) | 	app.GET(util.BasePath, handler.WireGuardClients(db), handler.ValidSession) | ||||||
| 
 |  | ||||||
| 	// 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(util.BasePath+"/login", handler.LoginPage()) | 		app.GET(util.BasePath+"/login", handler.LoginPage()) | ||||||
| 		app.POST(util.BasePath+"/login", handler.Login(db), handler.ContentTypeJson) | 		app.POST(util.BasePath+"/login", handler.Login(db)) | ||||||
| 		app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession) | 		app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession) | ||||||
| 		app.GET(util.BasePath+"/profile", handler.LoadProfile(), handler.ValidSession, handler.RefreshSession) | 		app.GET(util.BasePath+"/profile", handler.LoadProfile(db), handler.ValidSession) | ||||||
| 		app.GET(util.BasePath+"/users-settings", handler.UsersSettings(), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin) | 		app.GET(util.BasePath+"/users-settings", handler.UsersSettings(db), handler.ValidSession, handler.NeedsAdmin) | ||||||
| 		app.POST(util.BasePath+"/update-user", handler.UpdateUser(db), handler.ValidSession, handler.ContentTypeJson) | 		app.POST(util.BasePath+"/update-user", handler.UpdateUser(db), handler.ValidSession) | ||||||
| 		app.POST(util.BasePath+"/create-user", handler.CreateUser(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | 		app.POST(util.BasePath+"/create-user", handler.CreateUser(db), handler.ValidSession, handler.NeedsAdmin) | ||||||
| 		app.POST(util.BasePath+"/remove-user", handler.RemoveUser(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | 		app.POST(util.BasePath+"/remove-user", handler.RemoveUser(db), handler.ValidSession, handler.NeedsAdmin) | ||||||
| 		app.GET(util.BasePath+"/get-users", handler.GetUsers(db), handler.ValidSession, handler.NeedsAdmin) | 		app.GET(util.BasePath+"/getusers", handler.GetUsers(db), handler.ValidSession, handler.NeedsAdmin) | ||||||
| 		app.GET(util.BasePath+"/api/user/:username", handler.GetUser(db), handler.ValidSession) | 		app.GET(util.BasePath+"/api/user/:username", handler.GetUser(db), handler.ValidSession) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -231,7 +163,7 @@ func main() { | ||||||
| 	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.SmtpHelo, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption) | 		sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	app.GET(util.BasePath+"/test-hash", handler.GetHashesChanges(db), handler.ValidSession) | 	app.GET(util.BasePath+"/test-hash", handler.GetHashesChanges(db), handler.ValidSession) | ||||||
|  | @ -241,23 +173,21 @@ func main() { | ||||||
| 	app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson) | 	app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.POST(util.BasePath+"/update-client", handler.UpdateClient(db), handler.ValidSession, handler.ContentTypeJson) | 	app.POST(util.BasePath+"/update-client", handler.UpdateClient(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.POST(util.BasePath+"/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession, handler.ContentTypeJson) | 	app.POST(util.BasePath+"/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.POST(util.BasePath+"/send-telegram-client", handler.SendTelegramClient(db), handler.ValidSession, handler.ContentTypeJson) |  | ||||||
| 	app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson) | 	app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson) | 	app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession) | 	app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession) | ||||||
| 	app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin) | 	app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession, handler.NeedsAdmin) | ||||||
| 	app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | 	app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | ||||||
| 	app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | 	app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | ||||||
| 	app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin) | 	app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.NeedsAdmin) | ||||||
| 	app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | 	app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | ||||||
| 	app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession, handler.RefreshSession) | 	app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession) | ||||||
| 	app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession) | 	app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession) | ||||||
| 	app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession) | 	app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession) | ||||||
| 	app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession) | 	app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession) | ||||||
| 	app.GET(util.BasePath+"/api/subnet-ranges", handler.GetOrderedSubnetRanges(), handler.ValidSession) |  | ||||||
| 	app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession) | 	app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession) | ||||||
| 	app.POST(util.BasePath+"/api/apply-wg-config", handler.ApplyServerConfig(db, tmplDir), handler.ValidSession, handler.ContentTypeJson) | 	app.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.GET(util.BasePath+"/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession) | ||||||
| 	app.POST(util.BasePath+"/wake_on_lan_host", handler.SaveWakeOnLanHost(db), handler.ValidSession, handler.ContentTypeJson) | 	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.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) | 	app.PUT(util.BasePath+"/wake_on_lan_host/:mac_address", handler.WakeOnHost(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
|  | @ -269,36 +199,13 @@ func main() { | ||||||
| 	// serves other static files
 | 	// serves other static files
 | ||||||
| 	app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler))) | 	app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler))) | ||||||
| 
 | 
 | ||||||
| 	initDeps := telegram.TgBotInitDependencies{ | 	app.Logger.Fatal(app.Start(util.BindAddress)) | ||||||
| 		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) { | func initServerConfig(db store.IStore, tmplDir fs.FS) { | ||||||
| 	settings, err := db.GetGlobalSettings() | 	settings, err := db.GetGlobalSettings() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatalf("Cannot get global settings: %v", err) | 		log.Fatalf("Cannot get global settings: ", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := os.Stat(settings.ConfigFilePath); err == nil { | 	if _, err := os.Stat(settings.ConfigFilePath); err == nil { | ||||||
|  | @ -308,33 +215,22 @@ func initServerConfig(db store.IStore, tmplDir fs.FS) { | ||||||
| 
 | 
 | ||||||
| 	server, err := db.GetServer() | 	server, err := db.GetServer() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatalf("Cannot get server config: %v", err) | 		log.Fatalf("Cannot get server config: ", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	clients, err := db.GetClients(false) | 	clients, err := db.GetClients(false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatalf("Cannot get client config: %v", err) | 		log.Fatalf("Cannot get client config: ", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	users, err := db.GetUsers() | 	users, err := db.GetUsers() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatalf("Cannot get user config: %v", err) | 		log.Fatalf("Cannot get user config: ", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// write config file
 | 	// write config file
 | ||||||
| 	err = util.WriteWireGuardServerConfig(tmplDir, server, clients, users, settings) | 	err = util.WriteWireGuardServerConfig(tmplDir, server, clients, users, settings) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatalf("Cannot create server config: %v", err) | 		log.Fatalf("Cannot create server config: ", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 |  | ||||||
| func initTelegram(initDeps telegram.TgBotInitDependencies) { |  | ||||||
| 	go func() { |  | ||||||
| 		for { |  | ||||||
| 			err := telegram.Start(initDeps) |  | ||||||
| 			if err == nil { |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -11,14 +11,10 @@ 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"` | ||||||
|  | @ -32,7 +28,7 @@ type ClientData struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type QRCodeSettings struct { | type QRCodeSettings struct { | ||||||
| 	Enabled    bool | 	Enabled       bool | ||||||
| 	IncludeDNS bool | 	IncludeDNS    bool | ||||||
| 	IncludeMTU bool | 	IncludeMTU    bool | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -23,6 +23,5 @@ 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"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ package model | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"net" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
|  | @ -19,13 +18,7 @@ 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) | ||||||
| 	resourceName = strings.ReplaceAll(resourceName, ":", "-") | 	return strings.ReplaceAll(resourceName, ":", "-"), nil | ||||||
| 
 |  | ||||||
| 	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" | ||||||
|  |  | ||||||
|  | @ -48,17 +48,9 @@ func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // New function
 | // New function
 | ||||||
| func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo.Echo { | func New(tmplDir fs.FS, extraData map[string]interface{}, secret []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 := util.StringFromEmbedFile(tmplDir, "base.html") | 	tmplBaseString, err := util.StringFromEmbedFile(tmplDir, "base.html") | ||||||
|  |  | ||||||
|  | @ -6,7 +6,6 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
| 	"strconv" |  | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sdomino/scribble" | 	"github.com/sdomino/scribble" | ||||||
|  | @ -33,17 +32,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 = path.Join(o.dbPath, "clients") | 	var clientPath string = path.Join(o.dbPath, "clients") | ||||||
| 	var serverPath = path.Join(o.dbPath, "server") | 	var serverPath string = path.Join(o.dbPath, "server") | ||||||
| 	var userPath = path.Join(o.dbPath, "users") | 	var wakeOnLanHostsPath string = path.Join(o.dbPath, "wake_on_lan_hosts") | ||||||
| 	var wakeOnLanHostsPath = path.Join(o.dbPath, "wake_on_lan_hosts") | 	var serverInterfacePath string = path.Join(serverPath, "interfaces.json") | ||||||
| 	var serverInterfacePath = path.Join(serverPath, "interfaces.json") | 	var serverKeyPairPath string = path.Join(serverPath, "keypair.json") | ||||||
| 	var serverKeyPairPath = path.Join(serverPath, "keypair.json") | 	var globalSettingPath string = path.Join(serverPath, "global_settings.json") | ||||||
| 	var globalSettingPath = path.Join(serverPath, "global_settings.json") | 	var hashesPath string = path.Join(serverPath, "hashes.json") | ||||||
| 	var hashesPath = path.Join(serverPath, "hashes.json") | 	var userPath string = path.Join(serverPath, "users.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) { | ||||||
|  | @ -52,12 +52,12 @@ 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) | ||||||
| 	} | 	} | ||||||
|  | 	if _, err := os.Stat(userPath); os.IsNotExist(err) { | ||||||
|  | 		os.MkdirAll(userPath, os.ModePerm) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	// server's interface
 | 	// server's interface
 | ||||||
| 	if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) { | 	if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) { | ||||||
|  | @ -68,14 +68,11 @@ func (o *JsonDB) Init() error { | ||||||
| 		serverInterface.PostDown = util.LookupEnvOrString(util.ServerPostDownScriptEnvVar, "") | 		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 | ||||||
|  | @ -85,10 +82,6 @@ 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
 | ||||||
|  | @ -113,22 +106,14 @@ func (o *JsonDB) Init() error { | ||||||
| 		globalSetting.ConfigFilePath = util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath) | 		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
 | 	// hashes
 | ||||||
| 	if _, err := os.Stat(hashesPath); os.IsNotExist(err) { | 	if _, err := os.Stat(hashesPath); os.IsNotExist(err) { | ||||||
| 		clientServerHashes := new(model.ClientServerHashes) | 		clientServerHashes := new(model.ClientServerHashes) | ||||||
| 		clientServerHashes.Client = "none" | 		clientServerHashes.Client = "none" | ||||||
| 		clientServerHashes.Server = "none" | 		clientServerHashes.Server = "none" | ||||||
| 		o.conn.Write("server", "hashes", clientServerHashes) | 		o.conn.Write("server", "hashes", clientServerHashes) | ||||||
| 		err := util.ManagePerms(hashesPath) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// user info
 | 	// user info
 | ||||||
|  | @ -139,53 +124,25 @@ func (o *JsonDB) Init() error { | ||||||
| 		user.Admin = util.DefaultIsAdmin | 		user.Admin = util.DefaultIsAdmin | ||||||
| 		user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "") | 		user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "") | ||||||
| 		if user.PasswordHash == "" { | 		if user.PasswordHash == "" { | ||||||
| 			user.PasswordHash = util.LookupEnvOrFile(util.PasswordHashFileEnvVar, "") | 			plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword) | ||||||
| 			if user.PasswordHash == "" { | 			hash, err := util.HashPassword(plaintext) | ||||||
| 				plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword) | 			if err != nil { | ||||||
| 				if plaintext == util.DefaultPassword { | 				return err | ||||||
| 					plaintext = util.LookupEnvOrFile(util.PasswordFileEnvVar, util.DefaultPassword) |  | ||||||
| 				} |  | ||||||
| 				hash, err := util.HashPassword(plaintext) |  | ||||||
| 				if err != nil { |  | ||||||
| 					return err |  | ||||||
| 				} |  | ||||||
| 				user.PasswordHash = hash |  | ||||||
| 			} | 			} | ||||||
|  | 			user.PasswordHash = hash | ||||||
| 		} | 		} | ||||||
| 
 |  | ||||||
| 		o.conn.Write("users", user.Username, user) | 		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
 | ||||||
|  | func (o *JsonDB) GetUser() (model.User, error) { | ||||||
|  | 	user := model.User{} | ||||||
|  | 	return user, o.conn.Read("server", "users", &user) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // GetUsers func to get all users from the database
 | // GetUsers func to get all users from the database
 | ||||||
| func (o *JsonDB) GetUsers() ([]model.User, error) { | func (o *JsonDB) GetUsers() ([]model.User, error) { | ||||||
| 	var users []model.User | 	var users []model.User | ||||||
|  | @ -196,10 +153,11 @@ func (o *JsonDB) GetUsers() ([]model.User, error) { | ||||||
| 	for _, i := range results { | 	for _, i := range results { | ||||||
| 		user := model.User{} | 		user := model.User{} | ||||||
| 
 | 
 | ||||||
| 		if err := json.Unmarshal(i, &user); err != nil { | 		if err := json.Unmarshal([]byte(i), &user); err != nil { | ||||||
| 			return users, fmt.Errorf("cannot decode user json structure: %v", err) | 			return users, fmt.Errorf("cannot decode user json structure: %v", err) | ||||||
| 		} | 		} | ||||||
| 		users = append(users, user) | 		users = append(users, user) | ||||||
|  | 
 | ||||||
| 	} | 	} | ||||||
| 	return users, err | 	return users, err | ||||||
| } | } | ||||||
|  | @ -217,19 +175,11 @@ func (o *JsonDB) GetUserByName(username string) (model.User, error) { | ||||||
| 
 | 
 | ||||||
| // SaveUser func to save user in the database
 | // SaveUser func to save user in the database
 | ||||||
| func (o *JsonDB) SaveUser(user model.User) error { | func (o *JsonDB) SaveUser(user model.User) error { | ||||||
| 	userPath := path.Join(path.Join(o.dbPath, "users"), user.Username+".json") | 	return o.conn.Write("users", user.Username, user) | ||||||
| 	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
 | // DeleteUser func to remove user from the database
 | ||||||
| func (o *JsonDB) DeleteUser(username string) error { | func (o *JsonDB) DeleteUser(username string) error { | ||||||
| 	delete(util.DBUsersToCRC32, username) |  | ||||||
| 	return o.conn.Delete("users", username) | 	return o.conn.Delete("users", username) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -275,7 +225,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(f, &client); err != nil { | 		if err := json.Unmarshal([]byte(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) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -286,7 +236,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(png) | 				clientData.QRCode = "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte(png)) | ||||||
| 			} else { | 			} else { | ||||||
| 				fmt.Print("Cannot generate QR code: ", err) | 				fmt.Print("Cannot generate QR code: ", err) | ||||||
| 			} | 			} | ||||||
|  | @ -323,7 +273,7 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti | ||||||
| 
 | 
 | ||||||
| 		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(png) | 			clientData.QRCode = "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte(png)) | ||||||
| 		} else { | 		} else { | ||||||
| 			fmt.Print("Cannot generate QR code: ", err) | 			fmt.Print("Cannot generate QR code: ", err) | ||||||
| 		} | 		} | ||||||
|  | @ -335,59 +285,23 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (o *JsonDB) SaveClient(client model.Client) error { | func (o *JsonDB) SaveClient(client model.Client) error { | ||||||
| 	clientPath := path.Join(path.Join(o.dbPath, "clients"), client.ID+".json") | 	return o.conn.Write("clients", client.ID, client) | ||||||
| 	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 { | ||||||
| 	serverInterfacePath := path.Join(path.Join(o.dbPath, "server"), "interfaces.json") | 	return o.conn.Write("server", "interfaces", serverInterface) | ||||||
| 	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 { | ||||||
| 	serverKeyPairPath := path.Join(path.Join(o.dbPath, "server"), "keypair.json") | 	return o.conn.Write("server", "keypair", serverKeyPair) | ||||||
| 	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 { | ||||||
| 	globalSettingsPath := path.Join(path.Join(o.dbPath, "server"), "global_settings.json") | 	return o.conn.Write("server", "global_settings", globalSettings) | ||||||
| 	output := o.conn.Write("server", "global_settings", globalSettings) |  | ||||||
| 	err := util.ManagePerms(globalSettingsPath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return output |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (o *JsonDB) GetPath() string { | func (o *JsonDB) GetPath() string { | ||||||
|  | @ -400,11 +314,5 @@ func (o *JsonDB) GetHashes() (model.ClientServerHashes, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (o *JsonDB) SaveHashes(hashes model.ClientServerHashes) error { | func (o *JsonDB) SaveHashes(hashes model.ClientServerHashes) error { | ||||||
| 	hashesPath := path.Join(path.Join(o.dbPath, "server"), "hashes.json") | 	return o.conn.Write("server", "hashes", hashes) | ||||||
| 	output := o.conn.Write("server", "hashes", hashes) |  | ||||||
| 	err := util.ManagePerms(hashesPath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return output |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,10 +3,7 @@ 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) { | ||||||
|  | @ -23,7 +20,7 @@ func (o *JsonDB) GetWakeOnLanHosts() ([]model.WakeOnLanHost, error) { | ||||||
| 		host := model.WakeOnLanHost{} | 		host := model.WakeOnLanHost{} | ||||||
| 
 | 
 | ||||||
| 		// get client info
 | 		// get client info
 | ||||||
| 		if err := json.Unmarshal(f, &host); err != nil { | 		if err := json.Unmarshal([]byte(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) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -68,14 +65,7 @@ func (o *JsonDB) SaveWakeOnLanHost(host model.WakeOnLanHost) error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	wakeOnLanHostPath := path.Join(path.Join(o.dbPath, model.WakeOnLanHostCollectionName), resourceName+".json") | 	return o.conn.Write(model.WakeOnLanHostCollectionName, resourceName, host) | ||||||
| 	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 { | ||||||
|  |  | ||||||
							
								
								
									
										161
									
								
								telegram/bot.go
								
								
								
								
							
							
						
						
									
										161
									
								
								telegram/bot.go
								
								
								
								
							|  | @ -1,161 +0,0 @@ | ||||||
| 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) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -58,13 +58,11 @@ | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="form-group form-group-sm"> |                 <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;"> |                     <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="All">All</option> | ||||||
|                         <option value="Enabled">Enabled</option> |                         <option value="Enabled">Enabled</option> | ||||||
|                         <option value="Disabled">Disabled</option> |                         <option value="Disabled">Disabled</option> | ||||||
|                         <option value="Connected">Connected</option> |                         <option value="Connected">Connected</option> | ||||||
|                         <option value="Disconnected">Disconnected</option> |                         <option value="Disconnected">Disconnected</option> | ||||||
|                         <!-- THIS SECTION IS OVERRIDDEN BY JS. SEE updateSearchList() function in clients.html BEFORE EDITING --> |  | ||||||
|                     </select> |                     </select> | ||||||
|                 </div> |                 </div> | ||||||
|             </form> |             </form> | ||||||
|  | @ -211,12 +209,6 @@ | ||||||
|                                 <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"> | ||||||
|  | @ -240,10 +232,6 @@ | ||||||
|                                 </label> |                                 </label> | ||||||
|                                 <input type="text" data-role="tagsinput" class="form-control" id="client_extra_allowed_ips" value="{{ StringsJoin .client_defaults.ExtraAllowedIps "," }}"> |                                 <input type="text" data-role="tagsinput" class="form-control" id="client_extra_allowed_ips" value="{{ StringsJoin .client_defaults.ExtraAllowedIps "," }}"> | ||||||
|                             </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" {{ if .client_defaults.UseServerDNS }}checked{{ end }}> |                                     <input type="checkbox" id="use_server_dns" {{ if .client_defaults.UseServerDNS }}checked{{ end }}> | ||||||
|  | @ -281,18 +269,6 @@ | ||||||
|                                     <input type="text" class="form-control" id="client_preshared_key" name="client_preshared_key" placeholder="Autogenerated - enter "-" to skip generation"> |                                     <input type="text" class="form-control" id="client_preshared_key" name="client_preshared_key" placeholder="Autogenerated - enter "-" to skip generation"> | ||||||
|                                 </div> |                                 </div> | ||||||
|                             </details> |                             </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> | ||||||
|  | @ -388,34 +364,6 @@ | ||||||
| 
 | 
 | ||||||
|         $(document).ready(function () { |         $(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({ |                 $.ajax({ | ||||||
|                     cache: false, |                     cache: false, | ||||||
|                     method: 'GET', |                     method: 'GET', | ||||||
|  | @ -436,7 +384,9 @@ | ||||||
|                         toastr.error(responseJson['message']); |                         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. | ||||||
|  | @ -461,10 +411,8 @@ | ||||||
|         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 = []; | ||||||
| 
 | 
 | ||||||
|  | @ -484,12 +432,10 @@ | ||||||
|             } |             } | ||||||
|             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 additional_notes = $("#additional_notes").val(); |  | ||||||
| 
 | 
 | ||||||
|             const data = {"name": name, "email": email, "telegram_userid": telegram_userid, "allocated_ips": allocated_ips, "allowed_ips": allowed_ips, |             const data = {"name": name, "email": email, "allocated_ips": allocated_ips, "allowed_ips": allowed_ips, | ||||||
|                 "extra_allowed_ips": extra_allowed_ips, "endpoint": endpoint, "use_server_dns": use_server_dns, "enabled": enabled, |                 "extra_allowed_ips": extra_allowed_ips, "use_server_dns": use_server_dns, "enabled": enabled, | ||||||
|                 "public_key": public_key, "preshared_key": preshared_key, "additional_notes": additional_notes}; |                 "public_key": public_key, "preshared_key": preshared_key}; | ||||||
| 
 | 
 | ||||||
|             $.ajax({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|  | @ -505,7 +451,6 @@ | ||||||
|                     if (window.location.pathname === "{{.basePath}}/") { |                     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); | ||||||
|  | @ -516,32 +461,19 @@ | ||||||
| 
 | 
 | ||||||
|         // 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(forceDefault = false) { |         function updateIPAllocationSuggestion() { | ||||||
|             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: `{{.basePath}}/api/suggest-client-ips?sr=${subnetRange}`, |                 url: '{{.basePath}}/api/suggest-client-ips', | ||||||
|                 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']); | ||||||
|                 } |                 } | ||||||
|  | @ -560,7 +492,6 @@ | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|             'minInputWidth': '100%', |  | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -572,7 +503,6 @@ | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|             'minInputWidth': '100%', |  | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -583,7 +513,6 @@ | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|             'minInputWidth': '100%', |  | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -628,20 +557,10 @@ | ||||||
|                 $("#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(''); | ||||||
|                 $("#client_endpoint").val(''); |                 updateIPAllocationSuggestion(); | ||||||
|                 $("#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 () { | ||||||
|  | @ -652,7 +571,6 @@ | ||||||
|                     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'); | ||||||
|                     }, |                     }, | ||||||
|  |  | ||||||
|  | @ -80,35 +80,6 @@ Wireguard Clients | ||||||
| </div> | </div> | ||||||
| <!-- /.modal --> | <!-- /.modal --> | ||||||
| 
 | 
 | ||||||
| <div class="modal fade" id="modal_telegram_client"> |  | ||||||
|     <div class="modal-dialog"> |  | ||||||
|         <div class="modal-content"> |  | ||||||
|             <div class="modal-header"> |  | ||||||
|                 <h4 class="modal-title">Telegram Configuration</h4> |  | ||||||
|                 <button type="button" class="close" data-dismiss="modal" aria-label="Close"> |  | ||||||
|                     <span aria-hidden="true">×</span> |  | ||||||
|                 </button> |  | ||||||
|             </div> |  | ||||||
|             <form name="frm_telegram_client" id="frm_telegram_client"> |  | ||||||
|                 <div class="modal-body"> |  | ||||||
|                     <input type="hidden" id="tg_client_id" name="tg_client_id"> |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label for="tg_client_userid" class="control-label">Telegram userid</label> |  | ||||||
|                         <input type="text" class="form-control" id="tg_client_userid" name="tg_client_userid"> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="modal-footer justify-content-between"> |  | ||||||
|                     <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> |  | ||||||
|                     <button type="submit" class="btn btn-success">Send</button> |  | ||||||
|                 </div> |  | ||||||
|             </form> |  | ||||||
|         </div> |  | ||||||
|         <!-- /.modal-content --> |  | ||||||
|     </div> |  | ||||||
|     <!-- /.modal-dialog --> |  | ||||||
| </div> |  | ||||||
| <!-- /.modal --> |  | ||||||
| 
 |  | ||||||
| <div class="modal fade" id="modal_edit_client"> | <div class="modal fade" id="modal_edit_client"> | ||||||
|     <div class="modal-dialog"> |     <div class="modal-dialog"> | ||||||
|         <div class="modal-content"> |         <div class="modal-content"> | ||||||
|  | @ -129,12 +100,6 @@ 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"> | ||||||
|  | @ -148,10 +113,6 @@ 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"> | ||||||
|  | @ -168,38 +129,6 @@ 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> | ||||||
|  | @ -300,107 +229,17 @@ 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(); | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  | @ -416,7 +255,7 @@ Wireguard Clients | ||||||
|         // hide all clients and display only the ones that meet the search criteria (name, email, IP) |         // hide all clients and display only the ones that meet the search criteria (name, email, IP) | ||||||
|         $('#search-input').keyup(function () { |         $('#search-input').keyup(function () { | ||||||
|             $("#status-selector").val("All"); |             $("#status-selector").val("All"); | ||||||
|             let query = $(this).val().trim(); |             var query = $(this).val(); | ||||||
|             $('.col-lg-4').hide(); |             $('.col-lg-4').hide(); | ||||||
|             $(".info-box-text").each(function() { |             $(".info-box-text").each(function() { | ||||||
|                 if($(this).children('i.fa-user').length > 0 || $(this).children('i.fa-envelope').length > 0) |                 if($(this).children('i.fa-user').length > 0 || $(this).children('i.fa-envelope').length > 0) | ||||||
|  | @ -425,17 +264,6 @@ Wireguard Clients | ||||||
|                 } |                 } | ||||||
|                 }) |                 }) | ||||||
|             $(".badge-secondary").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 () { |         $("#status-selector").on('change', function () { | ||||||
|  | @ -497,18 +325,7 @@ Wireguard Clients | ||||||
|                     }); |                     }); | ||||||
|                     break; |                     break; | ||||||
|                 default: |                 default: | ||||||
|                     $('.col-lg-4').hide(); |                     $('.col-lg-4').show(); | ||||||
|                     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; |                     break; | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  | @ -559,7 +376,6 @@ 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); | ||||||
|  | @ -572,7 +388,6 @@ 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); | ||||||
|  | @ -587,7 +402,6 @@ Wireguard Clients | ||||||
|                     'defaultText': 'Add More', |                     'defaultText': 'Add More', | ||||||
|                     'removeWithBackspace': true, |                     'removeWithBackspace': true, | ||||||
|                     'minChars': 0, |                     'minChars': 0, | ||||||
|                     'minInputWidth': '100%', |  | ||||||
|                     'placeholderColor': '#666666' |                     'placeholderColor': '#666666' | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|  | @ -599,7 +413,6 @@ Wireguard Clients | ||||||
|                     'defaultText': 'Add More', |                     'defaultText': 'Add More', | ||||||
|                     'removeWithBackspace': true, |                     'removeWithBackspace': true, | ||||||
|                     'minChars': 0, |                     'minChars': 0, | ||||||
|                     'minInputWidth': '100%', |  | ||||||
|                     'placeholderColor': '#666666' |                     'placeholderColor': '#666666' | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|  | @ -610,7 +423,6 @@ Wireguard Clients | ||||||
|                     'defaultText': 'Add More', |                     'defaultText': 'Add More', | ||||||
|                     'removeWithBackspace' : true, |                     'removeWithBackspace' : true, | ||||||
|                     'minChars': 0, |                     'minChars': 0, | ||||||
|                     'minInputWidth': '100%', |  | ||||||
|                     'placeholderColor': '#666666' |                     'placeholderColor': '#666666' | ||||||
|                 }) |                 }) | ||||||
| 
 | 
 | ||||||
|  | @ -626,17 +438,9 @@ 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); | ||||||
|  | @ -652,20 +456,8 @@ 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); | ||||||
|  | @ -720,29 +512,8 @@ Wireguard Clients | ||||||
|                 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 | ||||||
|                 error: function(jqXHR, exception) { |                     location.reload(); | ||||||
|                     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); | ||||||
|  | @ -752,26 +523,19 @@ 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; | ||||||
|             } |             } | ||||||
|  | @ -782,11 +546,8 @@ Wireguard Clients | ||||||
|                 enabled = true; |                 enabled = true; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const additional_notes = $("#_additional_notes").val(); |             const data = {"id": client_id, "name": name, "email": email, "allocated_ips": allocated_ips, | ||||||
| 
 |                 "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, | ||||||
|  | @ -815,8 +576,6 @@ Wireguard Clients | ||||||
|                 submitEditClient(); |                 submitEditClient(); | ||||||
|             } else if (formId === "frm_email_client") { |             } else if (formId === "frm_email_client") { | ||||||
|                 submitEmailClient(); |                 submitEmailClient(); | ||||||
|             } else if (formId === "frm_telegram_client") { |  | ||||||
|                 submitTelegramClient(); |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -853,30 +612,6 @@ Wireguard Clients | ||||||
|             regenerateQRCode(); |             regenerateQRCode(); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         $("#modal_telegram_client").on('show.bs.modal', function (event) { |  | ||||||
|             let modal = $(this); |  | ||||||
|             const button = $(event.relatedTarget); |  | ||||||
|             const client_id = button.data('clientid'); |  | ||||||
|             $.ajax({ |  | ||||||
|                 cache: false, |  | ||||||
|                 method: 'GET', |  | ||||||
|                 url: '{{.basePath}}/api/client/' + client_id, |  | ||||||
|                 dataType: 'json', |  | ||||||
|                 contentType: "application/json", |  | ||||||
|                 success: function (resp) { |  | ||||||
|                     const client = resp.Client; |  | ||||||
| 
 |  | ||||||
|                     modal.find(".modal-title").text("Send config to client " + client.name); |  | ||||||
|                     modal.find("#tg_client_id").val(client.id); |  | ||||||
|                     modal.find("#tg_client_userid").val(client.telegram_userid); |  | ||||||
|                 }, |  | ||||||
|                 error: function (jqXHR, exception) { |  | ||||||
|                     const responseJson = jQuery.parseJSON(jqXHR.responseText); |  | ||||||
|                     toastr.error(responseJson['message']); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         $(document).ready(function () { |         $(document).ready(function () { | ||||||
|             $.validator.setDefaults({ |             $.validator.setDefaults({ | ||||||
|                 submitHandler: function (form) { |                 submitHandler: function (form) { | ||||||
|  | @ -932,32 +667,6 @@ 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> | ||||||
|  |  | ||||||
|  | @ -203,7 +203,6 @@ Global Settings | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|             'minInputWidth': '100%', |  | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -84,7 +84,7 @@ | ||||||
|     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 && /(?:^\/[a-zA-Z_])|(?:^\/$)/.test(nextURL.trim())) { |         if (nextURL) { | ||||||
|             window.location.href = nextURL; |             window.location.href = nextURL; | ||||||
|         } else { |         } else { | ||||||
|             window.location.href = '/{{.basePath}}'; |             window.location.href = '/{{.basePath}}'; | ||||||
|  |  | ||||||
|  | @ -42,12 +42,6 @@ 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" | ||||||
|  | @ -136,9 +130,8 @@ 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, "pre_down": pre_down, "post_down": post_down}; |             const data = {"addresses": addresses, "listen_port": listen_port, "post_up": post_up, "post_down": post_down}; | ||||||
| 
 | 
 | ||||||
|             $.ajax({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|  | @ -167,7 +160,6 @@ Wireguard Server Settings | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|             'minInputWidth': '100%', |  | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -96,7 +96,7 @@ Users Settings | ||||||
|         $.ajax({ |         $.ajax({ | ||||||
|             cache: false, |             cache: false, | ||||||
|             method: 'GET', |             method: 'GET', | ||||||
|             url: '{{.basePath}}/get-users', |             url: '{{.basePath}}/getusers', | ||||||
|             dataType: 'json', |             dataType: 'json', | ||||||
|             contentType: "application/json", |             contentType: "application/json", | ||||||
|             success: function (data) { |             success: function (data) { | ||||||
|  |  | ||||||
|  | @ -9,7 +9,6 @@ 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 }} | Table = {{ .globalSettings.Table }} | ||||||
| 
 | 
 | ||||||
|  | @ -17,17 +16,10 @@ Table = {{ .globalSettings.Table }} | ||||||
| # 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 }}{{end}} | {{if .Client.PresharedKey }}PresharedKey = {{ .Client.PresharedKey }} | ||||||
| AllowedIPs = {{$first :=true}}{{range .Client.AllocatedIPs }}{{if $first}}{{$first = false}}{{else}},{{end}}{{.}}{{end}}{{range .Client.ExtraAllowedIPs }},{{.}}{{end}} | {{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}} | ||||||
|  |  | ||||||
|  | @ -1,8 +0,0 @@ | ||||||
| package util |  | ||||||
| 
 |  | ||||||
| import "sync" |  | ||||||
| 
 |  | ||||||
| var IPToSubnetRange = map[string]uint16{} |  | ||||||
| var TgUseridToClientID = map[int64][]string{} |  | ||||||
| var TgUseridToClientIDMutex sync.RWMutex |  | ||||||
| var DBUsersToCRC32 = map[string]uint32{} |  | ||||||
|  | @ -1,33 +1,24 @@ | ||||||
| package util | package util | ||||||
| 
 | 
 | ||||||
| import ( | import "strings" | ||||||
| 	"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 | ||||||
| 	SmtpEncryption     string | 	SmtpEncryption string | ||||||
| 	SmtpAuthType       string | 	SmtpAuthType   string | ||||||
| 	SmtpHelo           string | 	SendgridApiKey string | ||||||
| 	SendgridApiKey     string | 	EmailFrom      string | ||||||
| 	EmailFrom          string | 	EmailFromName  string | ||||||
| 	EmailFromName      string | 	SessionSecret  []byte | ||||||
| 	SessionSecret      [64]byte | 	WgConfTemplate string | ||||||
| 	SessionMaxDuration int64 | 	BasePath       string | ||||||
| 	WgConfTemplate     string |  | ||||||
| 	BasePath           string |  | ||||||
| 	SubnetRanges       map[string]([]*net.IPNet) |  | ||||||
| 	SubnetRangesOrder  []string |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
|  | @ -39,14 +30,12 @@ const ( | ||||||
| 	DefaultDNS                             = "1.1.1.1" | 	DefaultDNS                             = "1.1.1.1" | ||||||
| 	DefaultMTU                             = 1450 | 	DefaultMTU                             = 1450 | ||||||
| 	DefaultPersistentKeepalive             = 15 | 	DefaultPersistentKeepalive             = 15 | ||||||
| 	DefaultFirewallMark                    = "0xca6c" // i.e. 51820
 | 	DefaultFirewallMark                    = "0xca6c"  // i.e. 51820
 | ||||||
| 	DefaultTable                           = "auto" | 	DefaultTable                           = "auto" | ||||||
| 	DefaultConfigFilePath                  = "/etc/wireguard/wg0.conf" | 	DefaultConfigFilePath                  = "/etc/wireguard/wg0.conf" | ||||||
| 	UsernameEnvVar                         = "WGUI_USERNAME" | 	UsernameEnvVar                         = "WGUI_USERNAME" | ||||||
| 	PasswordEnvVar                         = "WGUI_PASSWORD" | 	PasswordEnvVar                         = "WGUI_PASSWORD" | ||||||
| 	PasswordFileEnvVar                     = "WGUI_PASSWORD_FILE" |  | ||||||
| 	PasswordHashEnvVar                     = "WGUI_PASSWORD_HASH" | 	PasswordHashEnvVar                     = "WGUI_PASSWORD_HASH" | ||||||
| 	PasswordHashFileEnvVar                 = "WGUI_PASSWORD_HASH_FILE" |  | ||||||
| 	FaviconFilePathEnvVar                  = "WGUI_FAVICON_FILE_PATH" | 	FaviconFilePathEnvVar                  = "WGUI_FAVICON_FILE_PATH" | ||||||
| 	EndpointAddressEnvVar                  = "WGUI_ENDPOINT_ADDRESS" | 	EndpointAddressEnvVar                  = "WGUI_ENDPOINT_ADDRESS" | ||||||
| 	DNSEnvVar                              = "WGUI_DNS" | 	DNSEnvVar                              = "WGUI_DNS" | ||||||
|  | @ -75,45 +64,3 @@ func ParseBasePath(basePath string) string { | ||||||
| 	} | 	} | ||||||
| 	return 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 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -2,9 +2,7 @@ package util | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 
 |  | ||||||
| 	"golang.org/x/crypto/bcrypt" | 	"golang.org/x/crypto/bcrypt" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -22,7 +20,7 @@ func VerifyHash(base64Hash string, plaintext string) (bool, error) { | ||||||
| 		return false, fmt.Errorf("cannot decode base64 hash: %w", err) | 		return false, fmt.Errorf("cannot decode base64 hash: %w", err) | ||||||
| 	} | 	} | ||||||
| 	err = bcrypt.CompareHashAndPassword(hash, []byte(plaintext)) | 	err = bcrypt.CompareHashAndPassword(hash, []byte(plaintext)) | ||||||
| 	if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { | 	if err == bcrypt.ErrMismatchedHashAndPassword { | ||||||
| 		return false, nil | 		return false, nil | ||||||
| 	} | 	} | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  |  | ||||||
							
								
								
									
										381
									
								
								util/util.go
								
								
								
								
							
							
						
						
									
										381
									
								
								util/util.go
								
								
								
								
							|  | @ -1,16 +1,14 @@ | ||||||
| package util | package util | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bufio" |  | ||||||
| 	"bytes" |  | ||||||
| 	"encoding/gob" |  | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"hash/crc32" | 	"github.com/ngoduykhanh/wireguard-ui/store" | ||||||
|  | 	"golang.org/x/mod/sumdb/dirhash" | ||||||
| 	"io" | 	"io" | ||||||
| 	"io/fs" | 	"io/fs" | ||||||
| 	"math/rand" | 	"io/ioutil" | ||||||
| 	"net" | 	"net" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
|  | @ -20,23 +18,12 @@ import ( | ||||||
| 	"text/template" | 	"text/template" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"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
 | ||||||
|  | @ -105,15 +92,6 @@ func ClientDefaultsFromEnv() model.ClientDefaults { | ||||||
| 	return clientDefaults | 	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) | ||||||
|  | @ -191,7 +169,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 { | ||||||
|  | @ -232,9 +210,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("https://checkip.amazonaws.com/"), 1) | 	consensus.AddVoter(externalip.NewHTTPSource("http://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("https://ifconfig.top"), 1) | 	consensus.AddVoter(externalip.NewHTTPSource("http://ifconfig.top"), 1) | ||||||
| 
 | 
 | ||||||
| 	publicInterface := model.Interface{} | 	publicInterface := model.Interface{} | ||||||
| 	publicInterface.Name = "Public Address" | 	publicInterface.Name = "Public Address" | ||||||
|  | @ -246,7 +224,7 @@ func GetPublicIP() (model.Interface, error) { | ||||||
| 		publicInterface.IPAddress = ip.String() | 		publicInterface.IPAddress = ip.String() | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// error handling happened above, no need to pass it through
 | 	// error handling happend above, no need to pass it through
 | ||||||
| 	return publicInterface, nil | 	return publicInterface, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -294,7 +272,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(f, &client); err != nil { | 		if err := json.Unmarshal([]byte(f), &client); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -336,34 +314,17 @@ 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
 | ||||||
| // We need interfaceAddresses to find real broadcast and network addresses
 | func GetAvailableIP(cidr string, allocatedList []string) (string, error) { | ||||||
| func GetAvailableIP(cidr string, allocatedList, interfaceAddresses []string) (string, error) { | 	ip, net, err := net.ParseCIDR(cidr) | ||||||
| 	ip, netAddr, err := net.ParseCIDR(cidr) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	unavailableIPs := GetBroadcastAndNetworkAddrsLookup(interfaceAddresses) | 	broadcastAddr := GetBroadcastIP(net).String() | ||||||
|  | 	networkAddr := net.IP.String() | ||||||
| 
 | 
 | ||||||
| 	for ip := ip.Mask(netAddr.Mask); netAddr.Contains(ip); inc(ip) { | 	for ip := ip.Mask(net.Mask); net.Contains(ip); inc(ip) { | ||||||
| 		available := true | 		available := true | ||||||
| 		suggestedAddr := ip.String() | 		suggestedAddr := ip.String() | ||||||
| 		for _, allocatedAddr := range allocatedList { | 		for _, allocatedAddr := range allocatedList { | ||||||
|  | @ -372,7 +333,7 @@ func GetAvailableIP(cidr string, allocatedList, interfaceAddresses []string) (st | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		if available && !unavailableIPs[suggestedAddr] { | 		if available && suggestedAddr != networkAddr && suggestedAddr != broadcastAddr { | ||||||
| 			return suggestedAddr, nil | 			return suggestedAddr, nil | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -388,7 +349,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)
 | ||||||
|  | @ -400,7 +361,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 = false | 		var isValid bool = 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) { | ||||||
|  | @ -420,133 +381,13 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip | ||||||
| 	return true, nil | 	return true, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // findSubnetRangeForIP to find first SR for IP, and cache the match
 |  | ||||||
| func findSubnetRangeForIP(cidr string) (uint16, error) { |  | ||||||
| 	ip, _, err := net.ParseCIDR(cidr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return 0, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if srName, ok := IPToSubnetRange[ip.String()]; ok { |  | ||||||
| 		return srName, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for srIndex, sr := range SubnetRangesOrder { |  | ||||||
| 		for _, srCIDR := range SubnetRanges[sr] { |  | ||||||
| 			if srCIDR.Contains(ip) { |  | ||||||
| 				IPToSubnetRange[ip.String()] = uint16(srIndex) |  | ||||||
| 				return uint16(srIndex), nil |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return 0, fmt.Errorf("subnet range not found for this IP") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // FillClientSubnetRange to fill subnet ranges client belongs to, does nothing if SRs are not found
 |  | ||||||
| func FillClientSubnetRange(client model.ClientData) model.ClientData { |  | ||||||
| 	cl := *client.Client |  | ||||||
| 	for _, ip := range cl.AllocatedIPs { |  | ||||||
| 		sr, err := findSubnetRangeForIP(ip) |  | ||||||
| 		if err != nil { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		cl.SubnetRanges = append(cl.SubnetRanges, SubnetRangesOrder[sr]) |  | ||||||
| 	} |  | ||||||
| 	return model.ClientData{ |  | ||||||
| 		Client: &cl, |  | ||||||
| 		QRCode: client.QRCode, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ValidateAndFixSubnetRanges to check if subnet ranges are valid for the server configuration
 |  | ||||||
| // Removes all non-valid CIDRs
 |  | ||||||
| func ValidateAndFixSubnetRanges(db store.IStore) error { |  | ||||||
| 	if len(SubnetRangesOrder) == 0 { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	server, err := db.GetServer() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	var serverSubnets []*net.IPNet |  | ||||||
| 	for _, addr := range server.Interface.Addresses { |  | ||||||
| 		addr = strings.TrimSpace(addr) |  | ||||||
| 		_, netAddr, err := net.ParseCIDR(addr) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		serverSubnets = append(serverSubnets, netAddr) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, rng := range SubnetRangesOrder { |  | ||||||
| 		cidrs := SubnetRanges[rng] |  | ||||||
| 		if len(cidrs) > 0 { |  | ||||||
| 			newCIDRs := make([]*net.IPNet, 0) |  | ||||||
| 			for _, cidr := range cidrs { |  | ||||||
| 				valid := false |  | ||||||
| 
 |  | ||||||
| 				for _, serverSubnet := range serverSubnets { |  | ||||||
| 					if ContainsCIDR(serverSubnet, cidr) { |  | ||||||
| 						valid = true |  | ||||||
| 						break |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				if valid { |  | ||||||
| 					newCIDRs = append(newCIDRs, cidr) |  | ||||||
| 				} else { |  | ||||||
| 					log.Warnf("[%v] CIDR is outside of all server subnets: %v. Removed.", rng, cidr) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if len(newCIDRs) > 0 { |  | ||||||
| 				SubnetRanges[rng] = newCIDRs |  | ||||||
| 			} else { |  | ||||||
| 				delete(SubnetRanges, rng) |  | ||||||
| 				log.Warnf("[%v] No valid CIDRs in this subnet range. Removed.", rng) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // GetSubnetRangesString to get a formatted string, representing active subnet ranges
 |  | ||||||
| func GetSubnetRangesString() string { |  | ||||||
| 	if len(SubnetRangesOrder) == 0 { |  | ||||||
| 		return "" |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	strB := strings.Builder{} |  | ||||||
| 
 |  | ||||||
| 	for _, rng := range SubnetRangesOrder { |  | ||||||
| 		cidrs := SubnetRanges[rng] |  | ||||||
| 		if len(cidrs) > 0 { |  | ||||||
| 			strB.WriteString(rng) |  | ||||||
| 			strB.WriteString(":[") |  | ||||||
| 			first := true |  | ||||||
| 			for _, cidr := range cidrs { |  | ||||||
| 				if !first { |  | ||||||
| 					strB.WriteString(", ") |  | ||||||
| 				} |  | ||||||
| 				strB.WriteString(cidr.String()) |  | ||||||
| 				first = false |  | ||||||
| 			} |  | ||||||
| 			strB.WriteString("]  ") |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return strings.TrimSpace(strB.String()) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
 | // 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 { | func WriteWireGuardServerConfig(tmplDir fs.FS, serverConfig model.Server, clientDataList []model.ClientData, usersList []model.User, globalSettings model.GlobalSetting) error { | ||||||
| 	var tmplWireguardConf string | 	var tmplWireguardConf string | ||||||
| 
 | 
 | ||||||
| 	// if set, read wg.conf template from WgConfTemplate
 | 	// if set, read wg.conf template from WgConfTemplate
 | ||||||
| 	if len(WgConfTemplate) > 0 { | 	if len(WgConfTemplate) > 0 { | ||||||
| 		fileContentBytes, err := os.ReadFile(WgConfTemplate) | 		fileContentBytes, err := ioutil.ReadFile(WgConfTemplate) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  | @ -560,15 +401,6 @@ func WriteWireGuardServerConfig(tmplDir fs.FS, serverConfig model.Server, client | ||||||
| 		tmplWireguardConf = fileContent | 		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) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -583,7 +415,7 @@ func WriteWireGuardServerConfig(tmplDir fs.FS, serverConfig model.Server, client | ||||||
| 
 | 
 | ||||||
| 	config := map[string]interface{}{ | 	config := map[string]interface{}{ | ||||||
| 		"serverConfig":   serverConfig, | 		"serverConfig":   serverConfig, | ||||||
| 		"clientDataList": escapedClientDataList, | 		"clientDataList": clientDataList, | ||||||
| 		"globalSettings": globalSettings, | 		"globalSettings": globalSettings, | ||||||
| 		"usersList":      usersList, | 		"usersList":      usersList, | ||||||
| 	} | 	} | ||||||
|  | @ -597,57 +429,6 @@ func WriteWireGuardServerConfig(tmplDir fs.FS, serverConfig model.Server, client | ||||||
| 	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 | ||||||
|  | @ -684,20 +465,6 @@ func LookupEnvOrStrings(key string, defaultVal []string) []string { | ||||||
| 	return defaultVal | 	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) { | func StringFromEmbedFile(embed fs.FS, filename string) (string, error) { | ||||||
| 	file, err := embed.Open(filename) | 	file, err := embed.Open(filename) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -762,115 +529,3 @@ func UpdateHashes(db store.IStore) error { | ||||||
| 	clientServerHashes.Client, clientServerHashes.Server = GetCurrentHash(db) | 	clientServerHashes.Client, clientServerHashes.Server = GetCurrentHash(db) | ||||||
| 	return db.SaveHashes(clientServerHashes) | 	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