diff --git a/.dockerignore b/.dockerignore index 7624d3e..b9ff5d8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -25,6 +25,3 @@ docker-compose* db assets wireguard-ui - -# Examples -examples diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 28cee3f..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.html linguist-detectable=false diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index f8d96a4..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: [ngoduykhanh] diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 5e6b505..0000000 --- a/.github/stale.yml +++ /dev/null @@ -1,57 +0,0 @@ -# Configuration for probot-stale - https://github.com/probot/stale - -# Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 60 - -# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. -# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: 7 - -# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) -onlyLabels: [] - -# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable -exemptLabels: [] - -# Set to true to ignore issues in a project (defaults to false) -exemptProjects: false - -# Set to true to ignore issues in a milestone (defaults to false) -exemptMilestones: false - -# Set to true to ignore issues with an assignee (defaults to false) -exemptAssignees: false - -# Label to use when marking as stale -staleLabel: stale - -# Comment to post when marking as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. - -# Comment to post when closing a stale Issue or Pull Request. -# closeComment: > -# Your comment here. - -# Limit the number of actions per hour, from 1-30. Default is 30 -limitPerRun: 30 - -# Limit to only `issues` or `pulls` -only: issues - -# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': -# pulls: -# daysUntilStale: 30 -# markComment: > -# This pull request has been automatically marked as stale because it has not had -# recent activity. It will be closed if no further activity occurs. Thank you -# for your contributions. - -issues: - exemptLabels: - - enhancement - - feature request - - documentation - - bug diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml deleted file mode 100644 index 8960a48..0000000 --- a/.github/workflows/docker-build.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: Build container images - -on: - push: - branches: - - "master" - tags: - - "*" - -jobs: - build-image: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - # set environment - - name: Set BUILD_TIME env - run: echo "BUILD_TIME=$(date)" >> $GITHUB_ENV - - - name: Set GIT_COMMIT env - run: echo "GIT_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - - - name: Environment printer - uses: managedkaos/print-env@v1.0 - - - name: Prepare image tags - id: image-tags - run: | - base=ngoduykhanh/wireguard-ui - app_version=dev - - ## Set git tag as image tag - ## - if [[ '${{ github.ref }}' == *"refs/tags/"* ]]; then - github_tag="${GITHUB_REF#refs/*/}" - app_version=${github_tag} - - SEMVER_REGEX="^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(\\-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" - if [[ "$github_tag" =~ $SEMVER_REGEX ]]; then - github_tag=$(echo "${github_tag}" | sed 's/^v//') - fi - - container_images=$(cat <> $GITHUB_OUTPUT - echo "$container_images" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - ## Set APP_VERSION env - # - echo "APP_VERSION=${app_version}" >> $GITHUB_ENV - - # set up docker and build images - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v5 - with: - push: true - context: . - platforms: linux/amd64,linux/arm/v7,linux/arm64 - tags: ${{ steps.image-tags.outputs.container_images }} - build-args: | - APP_VERSION=${{ env.APP_VERSION }} - BUILD_TIME=${{ env.BUILD_TIME }} - GIT_COMMIT=${{ env.GIT_COMMIT }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 39fc51f..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4205bde..12b71ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,9 +7,8 @@ on: jobs: releases-matrix: name: Release Go Binary - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest strategy: - fail-fast: false matrix: # build and publish in parallel: linux/386, linux/amd64, darwin/386, darwin/amd64 goos: [linux, freebsd, darwin] @@ -25,28 +24,36 @@ jobs: - 7 steps: # get the source code - - uses: actions/checkout@v4 + - uses: actions/checkout@v2 # set environment - name: Set APP_VERSION env - run: echo "APP_VERSION=$(echo ${GITHUB_REF} | rev | cut -d'/' -f 1 | rev )" >> $GITHUB_ENV + run: echo ::set-env name=APP_VERSION::$(echo ${GITHUB_REF} | rev | cut -d'/' -f 1 | rev ) - name: Set BUILD_TIME env - run: echo "BUILD_TIME=$(date)" >> $GITHUB_ENV + run: echo ::set-env name=BUILD_TIME::$(date) - name: Environment Printer uses: managedkaos/print-env@v1.0 # setup node - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v1 with: - node-version: '20' + node-version: '10.x' registry-url: 'https://registry.npmjs.org' - # prepare assets + # prepare assets for go rice - name: Prepare assets run: | chmod +x ./prepare_assets.sh ./prepare_assets.sh + # get go rice tool + - name: Get go rice tool + run: go get github.com/GeertJohan/go.rice/rice + + # run go rice embed + - name: Run go rice embed + run: ${HOME}/go/bin/rice embed-go + # build and make the releases - name: Build and make the releases uses: wangyoucao577/go-release-action@master @@ -54,8 +61,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} - goversion: "https://dl.google.com/go/go1.21.5.linux-amd64.tar.gz" - pre_command: export CGO_ENABLED=0 + goversion: "https://dl.google.com/go/go1.14.2.linux-amd64.tar.gz" binary_name: "wireguard-ui" build_flags: -v ldflags: -X "main.appVersion=${{ env.APP_VERSION }}" -X "main.buildTime=${{ env.BUILD_TIME }}" -X main.gitCommit=${{ github.sha }} -X main.gitRef=${{ github.ref }} diff --git a/.gitignore b/.gitignore index 556c687..d6eba78 100644 --- a/.gitignore +++ b/.gitignore @@ -14,17 +14,10 @@ wireguard-ui # Dependency directories and files (remove the comment below to include it) vendor/ -assets/* -!assets/.gitkeep +assets/ node_modules/ +rice-box.go # IDEs .vscode .idea - -# Vim -.*.sw[op] - -# Examples -examples/docker-compose/config -examples/docker-compose/db diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index ead9b57..0000000 --- a/.golangci.yml +++ /dev/null @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 3b22f62..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -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. diff --git a/Dockerfile b/Dockerfile index 0a96884..88fa7e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,6 @@ # Build stage -FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.21-alpine3.19 AS builder -LABEL maintainer="Khanh Ngo " - -ARG BUILDPLATFORM -ARG TARGETOS -ARG TARGETARCH -ARG APP_VERSION=dev -ARG BUILD_TIME -ARG GIT_COMMIT - +FROM golang:1.14.2-alpine3.11 as builder +LABEL maintainer="Khanh Ngo ⚠️The default username and password are `admin`. Please change it to secure your setup. - -### Using binary file - -Download the binary file from the release page and run it directly on the host machine - -``` -./wireguard-ui -``` +Default username and password are `admin`. ### Using docker compose -The [examples/docker-compose](examples/docker-compose) folder contains example docker-compose files. -Choose the example which fits you the most, adjust the configuration for your needs, then run it like below: +You can take a look at this example of [docker-compose.yml](https://github.com/ngoduykhanh/wireguard-ui/blob/master/docker-compose.yaml). Please adjust volume mount points to work with your setup. Then run it like below: ``` docker-compose up ``` -## Environment Variables +### Using binary file -| Variable | Description | Default | -|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------| -| `BASE_PATH` | Set this variable if you run wireguard-ui under a subpath of your reverse proxy virtual host (e.g. /wireguard) | N/A | -| `BIND_ADDRESS` | The addresses that can access to the web interface and the port, use unix:///abspath/to/file.socket for unix domain socket. | 0.0.0.0:80 | -| `SESSION_SECRET` | The secret key used to encrypt the session cookies. Set this to a random value | N/A | -| `SESSION_SECRET_FILE` | Optional filepath for the secret key used to encrypt the session cookies. Leave `SESSION_SECRET` blank to take effect | N/A | -| `SESSION_MAX_DURATION` | Max time in days a remembered session is refreshed and valid. Non-refreshed session is valid for 7 days max, regardless of this setting. | 90 | -| `SUBNET_RANGES` | The list of address subdivision ranges. Format: `SR Name:10.0.1.0/24; SR2:10.0.2.0/24,10.0.3.0/24` Each CIDR must be inside one of the server interfaces. | N/A | -| `WGUI_USERNAME` | The username for the login page. Used for db initialization only | `admin` | -| `WGUI_PASSWORD` | The password for the user on the login page. Will be hashed automatically. Used for db initialization only | `admin` | -| `WGUI_PASSWORD_FILE` | Optional filepath for the user login password. Will be hashed automatically. Used for db initialization only. Leave `WGUI_PASSWORD` blank to take effect | N/A | -| `WGUI_PASSWORD_HASH` | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only | N/A | -| `WGUI_PASSWORD_HASH_FILE` | Optional filepath for the user login password hash. (alternative to `WGUI_PASSWORD_FILE`). Used for db initialization only. Leave `WGUI_PASSWORD_HASH` blank to take effect | N/A | -| `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings where clients should connect to. The endpoint can contain a port as well, useful when you are listening internally on the `WGUI_SERVER_LISTEN_PORT` port, but you forward on another port (ex 9000). Ex: myvpn.dyndns.com:9000 | Resolved to your public ip address | -| `WGUI_FAVICON_FILE_PATH` | The file path used as website favicon | Embedded WireGuard logo | -| `WGUI_DNS` | The default DNS servers (comma-separated-list) used in the global settings | `1.1.1.1` | -| `WGUI_MTU` | The default MTU used in global settings | `1450` | -| `WGUI_PERSISTENT_KEEPALIVE` | The default persistent keepalive for WireGuard in global settings | `15` | -| `WGUI_FIREWALL_MARK` | The default WireGuard firewall mark | `0xca6c` (51820) | -| `WGUI_TABLE` | The default WireGuard table value settings | `auto` | -| `WGUI_CONFIG_FILE_PATH` | The default WireGuard config file path used in global settings | `/etc/wireguard/wg0.conf` | -| `WGUI_LOG_LEVEL` | The default log level. Possible values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF` | `INFO` | -| `WG_CONF_TEMPLATE` | The custom `wg.conf` config file template. Please refer to our [default template](https://github.com/ngoduykhanh/wireguard-ui/blob/master/templates/wg.conf) | N/A | -| `EMAIL_FROM_ADDRESS` | The sender email address | N/A | -| `EMAIL_FROM_NAME` | The sender name | `WireGuard UI` | -| `SENDGRID_API_KEY` | The SendGrid api key | N/A | -| `SENDGRID_API_KEY_FILE` | Optional filepath for the SendGrid api key. Leave `SENDGRID_API_KEY` blank to take effect | N/A | -| `SMTP_HOSTNAME` | The SMTP IP address or hostname | `127.0.0.1` | -| `SMTP_PORT` | The SMTP port | `25` | -| `SMTP_USERNAME` | The SMTP username | N/A | -| `SMTP_PASSWORD` | The SMTP user password | N/A | -| `SMTP_PASSWORD_FILE` | Optional filepath for the SMTP user password. Leave `SMTP_PASSWORD` blank to take effect | N/A | -| `SMTP_AUTH_TYPE` | The SMTP authentication type. Possible values: `PLAIN`, `LOGIN`, `NONE` | `NONE` | -| `SMTP_ENCRYPTION` | The encryption method. Possible values: `NONE`, `SSL`, `SSLTLS`, `TLS`, `STARTTLS` | `STARTTLS` | -| `SMTP_HELO` | Hostname to use for the HELO message. smtp-relay.gmail.com needs this set to anything but `localhost` | `localhost` | -| `TELEGRAM_TOKEN` | Telegram bot token for distributing configs to clients | N/A | -| `TELEGRAM_ALLOW_CONF_REQUEST` | Allow users to get configs from the bot by sending a message | `false` | -| `TELEGRAM_FLOOD_WAIT` | Time in minutes before the next conf request is processed | `60` | +Download the binary file from the release and run it with command: -### Defaults for server configuration - -These environment variables are used to control the default server settings used when initializing the database. - -| Variable | Description | Default | -|-----------------------------------|-----------------------------------------------------------------------------------------------|-----------------| -| `WGUI_SERVER_INTERFACE_ADDRESSES` | The default interface addresses (comma-separated-list) for the WireGuard server configuration | `10.252.1.0/24` | -| `WGUI_SERVER_LISTEN_PORT` | The default server listen port | `51820` | -| `WGUI_SERVER_POST_UP_SCRIPT` | The default server post-up script | N/A | -| `WGUI_SERVER_POST_DOWN_SCRIPT` | The default server post-down script | N/A | - -### Defaults for new clients - -These environment variables are used to set the defaults used in `New Client` dialog. - -| Variable | Description | Default | -|---------------------------------------------|-------------------------------------------------------------------------------------------------|-------------| -| `WGUI_DEFAULT_CLIENT_ALLOWED_IPS` | Comma-separated-list of CIDRs for the `Allowed IPs` field. (default ) | `0.0.0.0/0` | -| `WGUI_DEFAULT_CLIENT_EXTRA_ALLOWED_IPS` | Comma-separated-list of CIDRs for the `Extra Allowed IPs` field. (default empty) | N/A | -| `WGUI_DEFAULT_CLIENT_USE_SERVER_DNS` | Boolean value [`0`, `f`, `F`, `false`, `False`, `FALSE`, `1`, `t`, `T`, `true`, `True`, `TRUE`] | `true` | -| `WGUI_DEFAULT_CLIENT_ENABLE_AFTER_CREATION` | Boolean value [`0`, `f`, `F`, `false`, `False`, `FALSE`, `1`, `t`, `T`, `true`, `True`, `TRUE`] | `true` | - -### Docker only - -These environment variables only apply to the docker container. - -| Variable | Description | Default | -|-----------------------|---------------------------------------------------------------|---------| -| `WGUI_MANAGE_START` | Start/stop WireGuard when the container is started/stopped | `false` | -| `WGUI_MANAGE_RESTART` | Auto restart WireGuard when we Apply Config changes in the UI | `false` | +``` +./wireguard-ui +``` ## Auto restart WireGuard daemon +WireGuard-UI only takes care of configuration generation. You can use systemd to watch for the changes and restart the service. Following is an example: -WireGuard-UI only takes care of configuration generation. You can use systemd to watch for the changes and restart the -service. Following is an example: +### systemd -### Using systemd +Create /etc/systemd/system/wgui.service -Create `/etc/systemd/system/wgui.service` - -```bash -cd /etc/systemd/system/ -cat << EOF > wgui.service +``` [Unit] Description=Restart WireGuard After=network.target @@ -125,17 +45,11 @@ After=network.target [Service] Type=oneshot ExecStart=/usr/bin/systemctl restart wg-quick@wg0.service - -[Install] -RequiredBy=wgui.path -EOF ``` -Create `/etc/systemd/system/wgui.path` +Create /etc/systemd/system/wgui.path -```bash -cd /etc/systemd/system/ -cat << EOF > wgui.path +``` [Unit] Description=Watch /etc/wireguard/wg0.conf for changes @@ -144,103 +58,81 @@ PathModified=/etc/wireguard/wg0.conf [Install] WantedBy=multi-user.target -EOF ``` Apply it -```sh +``` systemctl enable wgui.{path,service} systemctl start wgui.{path,service} ``` -### Using openrc +### openrc -Create `/usr/local/bin/wgui` file and make it executable - -```sh -cd /usr/local/bin/ -cat << EOF > wgui +Create and `chmod +x` /usr/local/bin/wgui +``` #!/bin/sh wg-quick down wg0 wg-quick up wg0 -EOF -chmod +x wgui ``` -Create `/etc/init.d/wgui` file and make it executable - -```sh -cd /etc/init.d/ -cat << EOF > wgui +Create and `chmod +x` /etc/init.d/wgui +``` #!/sbin/openrc-run command=/sbin/inotifyd command_args="/usr/local/bin/wgui /etc/wireguard/wg0.conf:w" pidfile=/run/${RC_SVCNAME}.pid command_background=yes -EOF -chmod +x wgui ``` Apply it -```sh +``` rc-service wgui start rc-update add wgui default ``` -### Using Docker - -Set `WGUI_MANAGE_RESTART=true` to manage Wireguard interface restarts. -Using `WGUI_MANAGE_START=true` can also replace the function of `wg-quick@wg0` service, to start Wireguard at boot, by -running the container with `restart: unless-stopped`. These settings can also pick up changes to Wireguard Config File -Path, after restarting the container. Please make sure you have `--cap-add=NET_ADMIN` in your container config to make -this feature work. - ## Build ### Build docker image Go to the project root directory and run the following command: -```sh -docker build --build-arg=GIT_COMMIT=$(git rev-parse --short HEAD) -t wireguard-ui . ``` - -or - -```sh -docker compose build --build-arg=GIT_COMMIT=$(git rev-parse --short HEAD) +docker build -t wireguard-ui . ``` -:information_source: A container image is available on [Docker Hub](https://hub.docker.com/r/ngoduykhanh/wireguard-ui) -which you can pull and use - -``` -docker pull ngoduykhanh/wireguard-ui -```` - ### Build binary file Prepare the assets directory -```sh +``` ./prepare_assets.sh ``` -Then build your executable +Then you can embed resources by generating Go source code -```sh +``` +rice embed-go go build -o wireguard-ui ``` -## License +Or, append resources to executable as zip file +``` +go build -o wireguard-ui +rice append --exec wireguard-ui +``` + +## Screenshot + +![wireguard-ui](https://user-images.githubusercontent.com/6447444/80270680-76adf980-86e4-11ea-8ca1-9237f0dfa249.png) + +## License MIT. See [LICENSE](https://github.com/ngoduykhanh/wireguard-ui/blob/master/LICENSE). ## Support - If you like the project and want to support it, you can *buy me a coffee* ☕ Buy Me A Coffee diff --git a/assets/.gitkeep b/assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/custom/img/favicon.ico b/custom/img/favicon.ico deleted file mode 100644 index 7852f45..0000000 Binary files a/custom/img/favicon.ico and /dev/null differ diff --git a/custom/js/helper.js b/custom/js/helper.js index 5b43272..42bc17a 100644 --- a/custom/js/helper.js +++ b/custom/js/helper.js @@ -1,20 +1,5 @@ function renderClientList(data) { $.each(data, function(index, obj) { - // render telegram button - let telegramButton = '' - if (obj.Client.telegram_userid) { - telegramButton = `
- -
` - } - - let telegramHtml = ""; - if (obj.Client.telegram_userid && obj.Client.telegram_userid.length > 0) { - telegramHtml = `` - } - // render client status css tag style let clientStatusHtml = '>' if (obj.Client.enabled) { @@ -33,69 +18,34 @@ function renderClientList(data) { allowedIpsHtml += `${obj} `; }) - 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 = `` - } - // render client html content - let html = `
+ let html = `
-
+ +
- Download -
-
+ -
-
- -
- ${telegramButton} -
- - - + data-clientname="${obj.Client.name}">Remove

${obj.Client.name} - - - ${telegramHtml} - ${additionalNotesHtml} ${obj.Client.email} - ${prettyDateTime(obj.Client.created_at)} + ${obj.Client.created_at} - ${prettyDateTime(obj.Client.updated_at)} - - ${obj.Client.use_server_dns ? 'DNS enabled' : 'DNS disabled'} - - ${obj.Client.additional_notes} + ${obj.Client.updated_at} IP Allocation` + allocatedIpsHtml + `Allowed IPs` @@ -108,38 +58,3 @@ function renderClientList(data) { $('#client-list').append(html); }); } - -function renderUserList(data) { - $.each(data, function(index, obj) { - let clientStatusHtml = '>' - - // render user html content - let html = `
-
-
-
- -
-
- -
-
- ${obj.username} - ${obj.admin? 'Administrator':'Manager'} -
-
-
` - - // add the user html elements to the list - $('#users-list').append(html); - }); -} - - -function prettyDateTime(timeStr) { - const dt = new Date(timeStr); - const offsetMs = dt.getTimezoneOffset() * 60 * 1000; - const dateLocal = new Date(dt.getTime() - offsetMs); - return dateLocal.toISOString().slice(0, 19).replace(/-/g, "/").replace("T", " "); -} diff --git a/custom/js/wake_on_lan_hosts.js b/custom/js/wake_on_lan_hosts.js deleted file mode 100644 index 36ad1f3..0000000 --- a/custom/js/wake_on_lan_hosts.js +++ /dev/null @@ -1,210 +0,0 @@ -var base_url = jQuery(".brand-link").attr('href'); -if (base_url.substring(base_url.length - 1, base_url.length) != "/") - base_url = base_url + "/"; - - -const wake_on_lan_new_template = '
\n' + - '\t
\n' + - '\t\t
\n' + - '\t\t\t
\n' + - '\t\t\t\t\n' + - '\t\t\t\t\n' + - '\t\t\t\t\n' + - '\t\t\t
\n' + - '\t\t\t
\n' + - '\t\t\t {{ .Name }}\n' + - '\t\t\t {{ .MacAddress }}\n' + - '\t\t\t Unused\n' + - '\t\t
\n' + - '\t
\n' + - '
'; - -jQuery(function ($) { - $.validator.addMethod('mac', function (value, element) { - return this.optional(element) || /^([0-9A-F]{2}[:]){5}([0-9A-F]{2})$/.test(value); - }, 'Please enter a valid MAC Address.(uppercase letters and numbers, : only) ex: 00:AB:12:EF:DD:AA'); -}); - -jQuery.each(["put", "delete"], function (i, method) { - jQuery[method] = function (url, data, callback, type) { - if (jQuery.isFunction(data)) { - type = type || callback; - callback = data; - data = undefined; - } - - return jQuery.ajax({ - url: url, - type: method, - dataType: type, - data: data, - success: callback, - contentType: 'application/json' - }); - }; -}); - -jQuery(function ($) { - let newHostHtml = '
'; - $('h1').parents(".row").append(newHostHtml); -}); - -jQuery(function ($) { - $('.btn-outline-success').click(function () { - const $this = $(this); - $.put(base_url + 'wake_on_lan_host/' + $this.data('mac-address'), function (result) { - $this.parents('.info-box').find('.latest-used').text(prettyDateTime(result)); - }); - }); -}); - -jQuery(function ($) { - let $modal_remove_wake_on_lan_host = $('#modal_remove_wake_on_lan_host'); - let $remove_client_confirm = $('#remove_wake_on_host_confirm'); - - $modal_remove_wake_on_lan_host.on('show.bs.modal', function (event) { - const $btn = $(event.relatedTarget); - const $modal = $(this); - - const $editBtn = $btn.parents('.btn-group').find('.btn_modify_wake_on_lan_host'); - $modal.find('.modal-body').text("You are about to remove Wake On Lan Host " + $editBtn.data('name')); - $remove_client_confirm.val($editBtn.data('mac-address')); - }) - - $remove_client_confirm.click(function () { - const macAddress = $remove_client_confirm.val().replaceAll(":", "-"); - $.delete(base_url + 'wake_on_lan_host/' + macAddress); - $('#' + macAddress).remove(); - - $modal_remove_wake_on_lan_host.modal('hide'); - }); -}); - -jQuery(function ($) { - $('.latest-used').each(function () { - const $this = $(this); - const timeText = $this.text().trim(); - try { - if (timeText != "Unused") { - $this.text(prettyDateTime(timeText)); - } - } catch (ex) { - console.log(timeText); - throw ex; - } - }); -}); - -jQuery(function ($) { - let $modal_wake_on_lan_host = $("#modal_wake_on_lan_host"); - let $name = $('#frm_wake_on_lan_host_name'); - let $macAddress = $('#frm_wake_on_lan_host_mac_address'); - let $oldMacAddress = $('#frm_wake_on_lan_host_old_mac_address'); - let $contentRow = $('.content .row'); - let $frm_wake_on_lan_host = $("#frm_wake_on_lan_host"); - - // https://jqueryvalidation.org/ - let validator = $frm_wake_on_lan_host.validate({ - submitHandler: function () { - let data = { - name: $name.val(), - mac_address: $macAddress.val().toUpperCase(), - old_mac_address: $oldMacAddress.val().toUpperCase() - }; - $.ajax({ - cache: false, - method: 'POST', - url: base_url + 'wake_on_lan_host', - dataType: 'json', - contentType: "application/json", - data: JSON.stringify(data), - success: function (response) { - /** @type {string} */ - let oldMacAddress = $oldMacAddress.val().toUpperCase(); - - if (oldMacAddress != '') { - let macAddress = response.MacAddress; - let name = response.Name; - - let $container = $('#' + oldMacAddress.replaceAll(":", "-")); - if (macAddress != oldMacAddress) { - $container.attr('id', macAddress.replaceAll(":", "-")); - $container.find('.mac-address').text(macAddress); - $container.find('[data-mac-address]').data('mac-address', macAddress); - } - - $container.find('.name').text(name); - $container.find('[data-name]').data('name', name); - } else { - const $template = $( - wake_on_lan_new_template - .replace(/{{ .Id }}/g, response.MacAddress.replaceAll(":", "-").toUpperCase()) - .replace(/{{ .MacAddress }}/g, response.MacAddress.toUpperCase()) - .replace(/{{ .Name }}/g, response.Name) - ); - - $contentRow.append($template); - } - $modal_wake_on_lan_host.modal('hide'); - toastr.success('Wake on Lan Host Save successfully'); - }, - error: function (jqXHR, exception) { - const responseJson = jQuery.parseJSON(jqXHR.responseText); - toastr.error(responseJson['message']); - - if (typeof (console) != 'undefined') - console.log(exception); - } - }); - - return false; - }, - rules: { - name: { - required: true, - }, - mac_address: { - required: true, - mac: true, - } - }, - messages: { - name: { - required: "Please enter a name" - }, - mac_address: { - required: "Please enter a Mac Address" - } - }, - errorElement: 'span', - errorPlacement: function (error, element) { - error.addClass('invalid-feedback'); - element.closest('.form-group').append(error); - }, - highlight: function (element) { - $(element).addClass('is-invalid'); - }, - unhighlight: function (element) { - $(element).removeClass('is-invalid'); - } - }); - - $modal_wake_on_lan_host.on('show.bs.modal', function (e) { - const $btn = $(e.relatedTarget); - validator.resetForm(); - $macAddress.removeClass('is-invalid'); - - $name.val($btn.data('name')); - $macAddress.val($btn.data('mac-address')); - $oldMacAddress.val($btn.data('mac-address')); - }); -}); diff --git a/docker-compose.yaml b/docker-compose.yaml index a7d49c0..72da096 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,23 +1,11 @@ -version: "3" +version: '3' services: wg: - build: . - #image: ngoduykhanh/wireguard-ui:latest + image: ngoduykhanh/wireguard-ui:latest container_name: wgui - cap_add: - - NET_ADMIN - network_mode: host - environment: - - SENDGRID_API_KEY - - EMAIL_FROM_ADDRESS - - EMAIL_FROM_NAME - - SESSION_SECRET - - WGUI_USERNAME=alpha - - WGUI_PASSWORD=this-unusual-password - - WG_CONF_TEMPLATE - - WGUI_MANAGE_START=false - - WGUI_MANAGE_RESTART=false + ports: + - 5000:5000 logging: driver: json-file options: diff --git a/emailer/interface.go b/emailer/interface.go deleted file mode 100644 index 5a486fc..0000000 --- a/emailer/interface.go +++ /dev/null @@ -1,10 +0,0 @@ -package emailer - -type Attachment struct { - Name string - Data []byte -} - -type Emailer interface { - Send(toName string, to string, subject string, content string, attachments []Attachment) error -} diff --git a/emailer/sendgrid.go b/emailer/sendgrid.go deleted file mode 100644 index 864c953..0000000 --- a/emailer/sendgrid.go +++ /dev/null @@ -1,54 +0,0 @@ -package emailer - -import ( - "encoding/base64" - - "github.com/sendgrid/sendgrid-go" - "github.com/sendgrid/sendgrid-go/helpers/mail" -) - -type SendgridApiMail struct { - apiKey string - fromName string - from string -} - -func NewSendgridApiMail(apiKey, fromName, from string) *SendgridApiMail { - ans := SendgridApiMail{apiKey: apiKey, fromName: fromName, from: from} - return &ans -} - -func (o *SendgridApiMail) Send(toName string, to string, subject string, content string, attachments []Attachment) error { - m := mail.NewV3Mail() - - mailFrom := mail.NewEmail(o.fromName, o.from) - mailContent := mail.NewContent("text/html", content) - mailTo := mail.NewEmail(toName, to) - - m.SetFrom(mailFrom) - m.AddContent(mailContent) - - personalization := mail.NewPersonalization() - personalization.AddTos(mailTo) - personalization.Subject = subject - - m.AddPersonalizations(personalization) - - toAdd := make([]*mail.Attachment, 0, len(attachments)) - for i := range attachments { - var att mail.Attachment - encoded := base64.StdEncoding.EncodeToString(attachments[i].Data) - att.SetContent(encoded) - att.SetType("text/plain") - att.SetFilename(attachments[i].Name) - att.SetDisposition("attachment") - toAdd = append(toAdd, &att) - } - - m.AddAttachment(toAdd...) - request := sendgrid.GetRequest(o.apiKey, "/v3/mail/send", "https://api.sendgrid.com") - request.Method = "POST" - request.Body = mail.GetRequestBody(m) - _, err := sendgrid.API(request) - return err -} diff --git a/emailer/smtp.go b/emailer/smtp.go deleted file mode 100644 index 2586924..0000000 --- a/emailer/smtp.go +++ /dev/null @@ -1,100 +0,0 @@ -package emailer - -import ( - "crypto/tls" - "fmt" - "strings" - "time" - - mail "github.com/xhit/go-simple-mail/v2" -) - -type SmtpMail struct { - hostname string - port int - username string - password string - smtpHelo string - authType mail.AuthType - encryption mail.Encryption - noTLSCheck bool - fromName string - from string -} - -func authType(authType string) mail.AuthType { - switch strings.ToUpper(authType) { - case "PLAIN": - return mail.AuthPlain - case "LOGIN": - return mail.AuthLogin - default: - return mail.AuthNone - } -} - -func encryptionType(encryptionType string) mail.Encryption { - switch strings.ToUpper(encryptionType) { - case "NONE": - return mail.EncryptionNone - case "SSL": - return mail.EncryptionSSL - case "SSLTLS": - return mail.EncryptionSSLTLS - case "TLS": - return mail.EncryptionTLS - default: - return mail.EncryptionSTARTTLS - } -} - -func NewSmtpMail(hostname string, port int, username string, password string, SmtpHelo string, noTLSCheck bool, auth string, fromName, from string, encryption string) *SmtpMail { - ans := SmtpMail{hostname: hostname, port: port, username: username, password: password, smtpHelo: SmtpHelo, noTLSCheck: noTLSCheck, fromName: fromName, from: from, authType: authType(auth), encryption: encryptionType(encryption)} - return &ans -} - -func addressField(address string, name string) string { - if name == "" { - return address - } - return fmt.Sprintf("%s <%s>", name, address) -} - -func (o *SmtpMail) Send(toName string, to string, subject string, content string, attachments []Attachment) error { - server := mail.NewSMTPClient() - - server.Host = o.hostname - server.Port = o.port - server.Authentication = o.authType - server.Username = o.username - server.Password = o.password - server.Helo = o.smtpHelo - server.Encryption = o.encryption - server.KeepAlive = false - server.ConnectTimeout = 10 * time.Second - server.SendTimeout = 10 * time.Second - - if o.noTLSCheck { - server.TLSConfig = &tls.Config{InsecureSkipVerify: true} - } - - smtpClient, err := server.Connect() - - if err != nil { - return err - } - - email := mail.NewMSG() - email.SetFrom(addressField(o.from, o.fromName)). - AddTo(addressField(to, toName)). - SetSubject(subject). - SetBody(mail.TextHTML, content) - - for _, v := range attachments { - email.Attach(&mail.File{Name: v.Name, Data: v.Data}) - } - - err = email.Send(smtpClient) - - return err -} diff --git a/examples/docker-compose/README.md b/examples/docker-compose/README.md deleted file mode 100644 index 951df08..0000000 --- a/examples/docker-compose/README.md +++ /dev/null @@ -1,30 +0,0 @@ -## Prerequisites - -### Kernel Module - -Depending on if the Wireguard kernel module is available on your system you have more or less choices which example to use. - -You can check if the kernel modules are available via the following command: -```shell -modprobe wireguard -``` - -If the command exits successfully and doesn't print an error the kernel modules are available. -If it does error, you either have to install them manually (or activate if deactivated) or use an userspace implementation. -For an example of an userspace implementation, see _borigtun_. - -### Credentials - -Username and password for all examples is `admin` by default. -For security reasons it's highly recommended to change them before the first startup. - -## Examples -- **[system](system.yml)** - - If you have Wireguard already installed on your system and only want to run the UI in docker this might fit the most. -- **[linuxserver](linuxserver.yml)** - - If you have the Wireguard kernel modules installed (included in the mainline kernel since version 5.6) but want it running inside of docker, this might fit the most. -- **[boringtun](boringtun.yml)** - - If Wireguard kernel modules are not available, you can switch to an userspace implementation like [boringtun](https://github.com/cloudflare/boringtun). diff --git a/examples/docker-compose/boringtun.yml b/examples/docker-compose/boringtun.yml deleted file mode 100644 index a1bdd2f..0000000 --- a/examples/docker-compose/boringtun.yml +++ /dev/null @@ -1,43 +0,0 @@ -version: "3" - -services: - boringtun: - image: ghcr.io/ntkme/boringtun:edge - command: - - wg0 - container_name: boringtun - # use the network of the 'wireguard-ui' service. this enables to show active clients in the status page - network_mode: service:wireguard-ui - cap_add: - - NET_ADMIN - volumes: - - /dev/net/tun:/dev/net/tun - - ./config:/etc/wireguard - - wireguard-ui: - image: ngoduykhanh/wireguard-ui:latest - container_name: wireguard-ui - cap_add: - - NET_ADMIN - environment: - - SENDGRID_API_KEY - - EMAIL_FROM_ADDRESS - - EMAIL_FROM_NAME - - SESSION_SECRET - - WGUI_USERNAME=admin - - WGUI_PASSWORD=admin - - WG_CONF_TEMPLATE - - WGUI_MANAGE_START=true - - WGUI_MANAGE_RESTART=true - logging: - driver: json-file - options: - max-size: 50m - volumes: - - ./db:/app/db - - ./config:/etc/wireguard - ports: - # port for wireguard-ui - - "5000:5000" - # port of the wireguard server. this must be set here as the `boringtun` container joins the network of this container and hasn't its own network over which it could publish the ports - - "51820:51820/udp" diff --git a/examples/docker-compose/linuxserver.yml b/examples/docker-compose/linuxserver.yml deleted file mode 100644 index 1b7a66f..0000000 --- a/examples/docker-compose/linuxserver.yml +++ /dev/null @@ -1,42 +0,0 @@ -version: "3" - -services: - wireguard: - image: linuxserver/wireguard:latest - container_name: wireguard - cap_add: - - NET_ADMIN - volumes: - - ./config:/config - ports: - # port for wireguard-ui. this must be set here as the `wireguard-ui` container joins the network of this container and hasn't its own network over which it could publish the ports - - "5000:5000" - # port of the wireguard server - - "51820:51820/udp" - - wireguard-ui: - image: ngoduykhanh/wireguard-ui:latest - container_name: wireguard-ui - depends_on: - - wireguard - cap_add: - - NET_ADMIN - # use the network of the 'wireguard' service. this enables to show active clients in the status page - network_mode: service:wireguard - environment: - - SENDGRID_API_KEY - - EMAIL_FROM_ADDRESS - - EMAIL_FROM_NAME - - SESSION_SECRET - - WGUI_USERNAME=admin - - WGUI_PASSWORD=admin - - WG_CONF_TEMPLATE - - WGUI_MANAGE_START=true - - WGUI_MANAGE_RESTART=true - logging: - driver: json-file - options: - max-size: 50m - volumes: - - ./db:/app/db - - ./config:/etc/wireguard diff --git a/examples/docker-compose/system.yml b/examples/docker-compose/system.yml deleted file mode 100644 index c27f31e..0000000 --- a/examples/docker-compose/system.yml +++ /dev/null @@ -1,27 +0,0 @@ -version: "3" - -services: - wireguard-ui: - image: ngoduykhanh/wireguard-ui:latest - container_name: wireguard-ui - cap_add: - - NET_ADMIN - # required to show active clients. with this set, you don't need to expose the ui port (5000) anymore - network_mode: host - environment: - - SENDGRID_API_KEY - - EMAIL_FROM_ADDRESS - - EMAIL_FROM_NAME - - SESSION_SECRET - - WGUI_USERNAME=admin - - WGUI_PASSWORD=admin - - WG_CONF_TEMPLATE - - WGUI_MANAGE_START=false - - WGUI_MANAGE_RESTART=false - logging: - driver: json-file - options: - max-size: 50m - volumes: - - ./db:/app/db - - /etc/wireguard:/etc/wireguard diff --git a/go.mod b/go.mod index e9647ca..a932024 100644 --- a/go.mod +++ b/go.mod @@ -1,52 +1,20 @@ module github.com/ngoduykhanh/wireguard-ui -go 1.21 +go 1.14 require ( - github.com/NicoNex/echotron/v3 v3.27.0 - github.com/glendc/go-external-ip v0.1.0 - github.com/gorilla/sessions v1.2.2 - github.com/labstack/echo-contrib v0.15.0 - github.com/labstack/echo/v4 v4.11.4 - github.com/labstack/gommon v0.4.2 - github.com/rs/xid v1.5.0 - github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d - github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8 - github.com/sendgrid/sendgrid-go v3.14.0+incompatible - github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - github.com/xhit/go-simple-mail/v2 v2.16.0 - golang.org/x/crypto v0.17.0 - golang.org/x/mod v0.14.0 - //golang.zx2c4.com/wireguard v0.0.20200121 // indirect - golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c + github.com/GeertJohan/go.rice v1.0.0 + github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/gorilla/sessions v1.2.0 + github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 // indirect + github.com/labstack/echo-contrib v0.9.0 + github.com/labstack/echo/v4 v4.1.16 + github.com/labstack/gommon v0.3.0 + github.com/leodido/go-urn v1.2.0 // indirect + github.com/rs/xid v1.2.1 + github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba + github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 + golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200324154536-ceff61240acf 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 -) diff --git a/go.sum b/go.sum index 3aa2ceb..cd95b58 100644 --- a/go.sum +++ b/go.sum @@ -1,188 +1,182 @@ -github.com/NicoNex/echotron/v3 v3.27.0 h1:iq4BLPO+Dz1JHjh2HPk0D0NldAZSYcAjaOicgYEhUzw= -github.com/NicoNex/echotron/v3 v3.27.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI= -github.com/coreos/bbolt v1.3.1-coreos.6.0.20180223184059-4f5275f4ebbf/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/casbin/casbin/v2 v2.0.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/glendc/go-external-ip v0.1.0 h1:iX3xQ2Q26atAmLTbd++nUce2P5ht5P4uD4V7caSY/xg= -github.com/glendc/go-external-ip v0.1.0/go.mod h1:CNx312s2FLAJoWNdJWZ2Fpf5O4oLsMFwuYviHjS4uJE= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= -github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd h1:1BzxHapafGJd/XlpMvocLeDBin2EKn90gXv2AQt5sfo= +github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd/go.mod h1:o9OoDQyE1WHvYVUH1FdFapy1/rCZHHq3O5wS4VA83ig= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= -github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= -github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= -github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= -github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= +github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8= github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw= -github.com/jessevdk/go-flags v0.0.0-20150816100521-1acbbaff2f34/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= -github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= -github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= -github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= -github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw= -github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs= -github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA= -github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U= -github.com/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU= -github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4= -github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= -github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= +github.com/labstack/echo-contrib v0.9.0 h1:hKBA2SnxdxR7sghH0J04zq/pImnKRmgvmQ6MvY9hug4= +github.com/labstack/echo-contrib v0.9.0/go.mod h1:TsFE5Vv0LRpZLoh4mMmaaAxzcTH+1CBFiUtVhwlegzU= +github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE= +github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o= +github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI= +github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4= +github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc= -github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= -github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= -github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= -github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8= -github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= -github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= -github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys= -github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8= -github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= -github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= -github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= -github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= -github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/sabhiram/go-colorize v0.0.0-20210403184538-366f55d711cf/go.mod h1:GvlEbMJBpbAXFn06UajbdBlGZ18iLvHyuIrgG//L8uk= -github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d h1:NDtoSmsxTpDYTqvUurn2ooAzDaYbJSB9/tOhLzaewgo= -github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d/go.mod h1:SVPBBd492Gk7Cq5lPd6OAYtIGk2r1FsyH8KT3IB8h7c= -github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8 h1:hlNRl87eAZhh2QMJVShuXHL6OOd0ObZM0JozDIruNeM= -github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8/go.mod h1:W6zxGUBCXRR5QugSd/nFcFVmwoGnvpjiNY/JwT03Wew= -github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= -github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= -github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA= -github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba h1:8QAc9wFAf2b/9cAXskm0wBylObZ0bTpRcaP7ThjLPVQ= +github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba/go.mod h1:W6zxGUBCXRR5QugSd/nFcFVmwoGnvpjiNY/JwT03Wew= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= +github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v0.0.0-20150929183540-2b15294402a8/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM= -github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/uber/jaeger-client-go v2.19.1-0.20191002155754-0be28c34dabf+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA= -github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= +github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190609082536-301114b31cce/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b h1:XDLXhn7ryprJVo+Lpkiib6CIuXE2031GDwtfEm7vLjI= -golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg= -golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto= -golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ= +golang.zx2c4.com/wireguard v0.0.20200121 h1:vcswa5Q6f+sylDfjqyrVNNrjsFUUbPsgAQTBCAg/Qf8= +golang.zx2c4.com/wireguard v0.0.20200121/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4= +golang.zx2c4.com/wireguard v0.0.20200320 h1:1vE6zVeO7fix9cJX1Z9ZQ+ikPIIx7vIyU0o0tLDD88g= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200324154536-ceff61240acf h1:rWUZHukj3poXegPQMZOXgxjTGIBe3mLNHNVvL5DsHus= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200324154536-ceff61240acf/go.mod h1:UdS9frhv65KTfwxME1xE8+rHYoFpbm36gOud1GhBe9c= +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/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/handler/middlewares.go b/handler/middlewares.go deleted file mode 100644 index b03ef46..0000000 --- a/handler/middlewares.go +++ /dev/null @@ -1,20 +0,0 @@ -package handler - -import ( - "net/http" - - "github.com/labstack/echo/v4" -) - -// ContentTypeJson checks that the requests have the Content-Type header set to "application/json". -// This helps against CSRF attacks. -func ContentTypeJson(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - contentType := c.Request().Header.Get("Content-Type") - if contentType != "application/json" { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Only JSON allowed"}) - } - - return next(c) - } -} diff --git a/handler/routes.go b/handler/routes.go index ede3654..28bc576 100644 --- a/handler/routes.go +++ b/handler/routes.go @@ -1,53 +1,24 @@ package handler import ( - "crypto/subtle" - "encoding/base64" "encoding/json" "fmt" - "io/fs" "net/http" - "os" - "regexp" - "sort" - "strconv" "strings" "time" + rice "github.com/GeertJohan/go.rice" + "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" - "github.com/rs/xid" - "github.com/skip2/go-qrcode" - "golang.zx2c4.com/wireguard/wgctrl" - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - - "github.com/ngoduykhanh/wireguard-ui/emailer" "github.com/ngoduykhanh/wireguard-ui/model" - "github.com/ngoduykhanh/wireguard-ui/store" - "github.com/ngoduykhanh/wireguard-ui/telegram" "github.com/ngoduykhanh/wireguard-ui/util" + "github.com/rs/xid" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) -var usernameRegexp = regexp.MustCompile("^\\w[\\w\\-.]*$") - -// Health check handler -func Health() echo.HandlerFunc { - return func(c echo.Context) error { - return c.String(http.StatusOK, "ok") - } -} - -func Favicon() echo.HandlerFunc { - return func(c echo.Context) error { - if favicon, ok := os.LookupEnv(util.FaviconFilePathEnvVar); ok { - return c.File(favicon) - } - return c.Redirect(http.StatusFound, util.BasePath+"/static/custom/img/favicon.ico") - } -} - // LoginPage handler func LoginPage() echo.HandlerFunc { return func(c echo.Context) error { @@ -56,78 +27,36 @@ func LoginPage() echo.HandlerFunc { } // Login for signing in handler -func Login(db store.IStore) echo.HandlerFunc { +func Login() echo.HandlerFunc { return func(c echo.Context) error { - data := make(map[string]interface{}) - err := json.NewDecoder(c.Request().Body).Decode(&data) + user := new(model.User) + c.Bind(user) + dbuser, err := util.GetUser() if err != nil { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"}) + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot query user from DB"}) } - username := data["username"].(string) - password := data["password"].(string) - rememberMe := data["rememberMe"].(bool) - - if !usernameRegexp.MatchString(username) { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) - } - - dbuser, err := db.GetUserByName(username) - if err != nil { - log.Infof("Cannot query user %s from DB", username) - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Invalid credentials"}) - } - - userCorrect := subtle.ConstantTimeCompare([]byte(username), []byte(dbuser.Username)) == 1 - - var passwordCorrect bool - if dbuser.PasswordHash != "" { - match, err := util.VerifyHash(dbuser.PasswordHash, password) - if err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify password"}) - } - passwordCorrect = match - } else { - passwordCorrect = subtle.ConstantTimeCompare([]byte(password), []byte(dbuser.Password)) == 1 - } - - if userCorrect && passwordCorrect { - ageMax := 0 - if rememberMe { - ageMax = 86400 * 7 - } - - cookiePath := util.GetCookiePath() - + if user.Username == dbuser.Username && user.Password == dbuser.Password { + // TODO: refresh the token sess, _ := session.Get("session", c) sess.Options = &sessions.Options{ - Path: cookiePath, - MaxAge: ageMax, + Path: "/", + MaxAge: 86400, HttpOnly: true, - SameSite: http.SameSiteLaxMode, } // set session_token tokenUID := xid.New().String() - now := time.Now().UTC().Unix() - sess.Values["username"] = dbuser.Username - sess.Values["user_hash"] = util.GetDBUserCRC32(dbuser) - sess.Values["admin"] = dbuser.Admin + sess.Values["username"] = user.Username 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()) // set session_token in cookie cookie := new(http.Cookie) cookie.Name = "session_token" - cookie.Path = cookiePath cookie.Value = tokenUID - cookie.MaxAge = ageMax - cookie.HttpOnly = true - cookie.SameSite = http.SameSiteLaxMode + cookie.Expires = time.Now().Add(24 * time.Hour) c.SetCookie(cookie) return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Logged in successfully"}) @@ -137,222 +66,21 @@ func Login(db store.IStore) echo.HandlerFunc { } } -// GetUsers handler return a JSON list of all users -func GetUsers(db store.IStore) echo.HandlerFunc { - return func(c echo.Context) error { - usersList, err := db.GetUsers() - if err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ - false, fmt.Sprintf("Cannot get user list: %v", err), - }) - } - - return c.JSON(http.StatusOK, usersList) - } -} - -// GetUser handler returns a JSON object of single user -func GetUser(db store.IStore) echo.HandlerFunc { - return func(c echo.Context) error { - username := c.Param("username") - - if !usernameRegexp.MatchString(username) { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) - } - - if !isAdmin(c) && (username != currentUser(c)) { - return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"}) - } - - userData, err := db.GetUserByName(username) - if err != nil { - return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "User not found"}) - } - - return c.JSON(http.StatusOK, userData) - } -} - // Logout to log a user out func Logout() echo.HandlerFunc { return func(c echo.Context) error { clearSession(c) - return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login") - } -} - -// LoadProfile to load user information -func LoadProfile() echo.HandlerFunc { - return func(c echo.Context) error { - return c.Render(http.StatusOK, "profile.html", map[string]interface{}{ - "baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c), Admin: isAdmin(c)}, - }) - } -} - -// UsersSettings handler -func UsersSettings() echo.HandlerFunc { - return func(c echo.Context) error { - return c.Render(http.StatusOK, "users_settings.html", map[string]interface{}{ - "baseData": model.BaseData{Active: "users-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, - }) - } -} - -// UpdateUser to update user information -func UpdateUser(db store.IStore) echo.HandlerFunc { - return func(c echo.Context) error { - data := make(map[string]interface{}) - err := json.NewDecoder(c.Request().Body).Decode(&data) - - if err != nil { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"}) - } - - username := data["username"].(string) - password := data["password"].(string) - previousUsername := data["previous_username"].(string) - admin := data["admin"].(bool) - - if !isAdmin(c) && (previousUsername != currentUser(c)) { - return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"}) - } - - if !isAdmin(c) { - admin = false - } - - if !usernameRegexp.MatchString(previousUsername) { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) - } - - user, err := db.GetUserByName(previousUsername) - if err != nil { - return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) - } - - if username == "" || !usernameRegexp.MatchString(username) { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) - } else { - user.Username = username - } - - if username != previousUsername { - _, err := db.GetUserByName(username) - if err == nil { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "This username is taken"}) - } - } - - if password != "" { - hash, err := util.HashPassword(password) - if err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) - } - user.PasswordHash = hash - } - - if previousUsername != currentUser(c) { - user.Admin = admin - } - - if err := db.DeleteUser(previousUsername); err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) - } - if err := db.SaveUser(user); err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) - } - log.Infof("Updated user information successfully") - - if previousUsername == currentUser(c) { - setUser(c, user.Username, user.Admin, util.GetDBUserCRC32(user)) - } - - return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated user information successfully"}) - } -} - -// CreateUser to create new user -func CreateUser(db store.IStore) echo.HandlerFunc { - return func(c echo.Context) error { - data := make(map[string]interface{}) - err := json.NewDecoder(c.Request().Body).Decode(&data) - - if err != nil { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"}) - } - - var user model.User - username := data["username"].(string) - password := data["password"].(string) - admin := data["admin"].(bool) - - if username == "" || !usernameRegexp.MatchString(username) { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) - } else { - user.Username = username - } - - { - _, err := db.GetUserByName(username) - if err == nil { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "This username is taken"}) - } - } - - hash, err := util.HashPassword(password) - if err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) - } - user.PasswordHash = hash - - user.Admin = admin - - if err := db.SaveUser(user); err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) - } - log.Infof("Created user successfully") - - return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Created user successfully"}) - } -} - -// RemoveUser handler -func RemoveUser(db store.IStore) echo.HandlerFunc { - return func(c echo.Context) error { - data := make(map[string]interface{}) - err := json.NewDecoder(c.Request().Body).Decode(&data) - - if err != nil { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"}) - } - - username := data["username"].(string) - - if !usernameRegexp.MatchString(username) { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) - } - - if username == currentUser(c) { - return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "User cannot delete itself"}) - } - // delete user from database - - if err := db.DeleteUser(username); err != nil { - log.Error("Cannot delete user: ", err) - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot delete user from database"}) - } - - log.Infof("Removed user: %s", username) - - return c.JSON(http.StatusOK, jsonHTTPResponse{true, "User removed"}) + return c.Redirect(http.StatusTemporaryRedirect, "/login") } } // WireGuardClients handler -func WireGuardClients(db store.IStore) echo.HandlerFunc { +func WireGuardClients() echo.HandlerFunc { return func(c echo.Context) error { - clientDataList, err := db.GetClients(true) + // access validation + validSession(c) + + clientDataList, err := util.GetClients(true) if err != nil { return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ false, fmt.Sprintf("Cannot get client list: %v", err), @@ -360,78 +88,69 @@ func WireGuardClients(db store.IStore) echo.HandlerFunc { } return c.Render(http.StatusOK, "clients.html", map[string]interface{}{ - "baseData": model.BaseData{Active: "", CurrentUser: currentUser(c), Admin: isAdmin(c)}, + "baseData": model.BaseData{Active: "", CurrentUser: currentUser(c)}, "clientDataList": clientDataList, }) } } -// GetClients handler return a JSON list of Wireguard client data -func GetClients(db store.IStore) echo.HandlerFunc { +// GetClients handler return a list of Wireguard client data +func GetClients() echo.HandlerFunc { return func(c echo.Context) error { - clientDataList, err := db.GetClients(true) + // access validation + validSession(c) + + clientDataList, err := util.GetClients(true) if err != nil { return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ false, fmt.Sprintf("Cannot get client list: %v", err), }) } - for i, clientData := range clientDataList { - clientDataList[i] = util.FillClientSubnetRange(clientData) - } - return c.JSON(http.StatusOK, clientDataList) } } -// GetClient handler returns a JSON object of Wireguard client data -func GetClient(db store.IStore) echo.HandlerFunc { +// GetClient handler return a of Wireguard client data +func GetClient() echo.HandlerFunc { return func(c echo.Context) error { + // access validation + validSession(c) + 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{ - Enabled: true, - IncludeDNS: true, - IncludeMTU: true, - } - - clientData, err := db.GetClientByID(clientID, qrCodeSettings) + clientData, err := util.GetClientByID(clientID, true) if err != nil { 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 -func NewClient(db store.IStore) echo.HandlerFunc { +func NewClient() echo.HandlerFunc { return func(c echo.Context) error { - var client model.Client - c.Bind(&client) + // access validation + validSession(c) - // 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"}) - } + client := new(model.Client) + c.Bind(client) + + db, err := util.DBConn() + if err != nil { + log.Error("Cannot initialize database: ", err) + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot access database"}) } // read server information - server, err := db.GetServer() - if err != nil { - log.Error("Cannot fetch server from database: ", err) - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) + serverInterface := model.ServerInterface{} + if err := db.Read("server", "interfaces", &serverInterface); err != nil { + log.Error("Cannot fetch server interface config from database: ", err) } // validate the input Allocation IPs allocatedIPs, err := util.GetAllocatedIPs("") - check, err := util.ValidateIPAllocation(server.Interface.Addresses, allocatedIPs, client.AllocatedIPs) + check, err := util.ValidateIPAllocation(serverInterface.Addresses, allocatedIPs, client.AllocatedIPs) if !check { return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, fmt.Sprintf("%s", err)}) } @@ -442,218 +161,72 @@ func NewClient(db store.IStore) echo.HandlerFunc { return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Allowed IPs must be in CIDR format"}) } - // validate extra AllowedIPs - if util.ValidateExtraAllowedIPs(client.ExtraAllowedIPs) == false { - log.Warnf("Invalid Extra AllowedIPs input from user: %v", client.ExtraAllowedIPs) - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Extra AllowedIPs must be in CIDR format"}) - } - // gen ID guid := xid.New() client.ID = guid.String() // gen Wireguard key pair - if client.PublicKey == "" { - key, err := wgtypes.GeneratePrivateKey() - if err != nil { - log.Error("Cannot generate wireguard key pair: ", err) - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot generate Wireguard key pair"}) - } - client.PrivateKey = key.String() - client.PublicKey = key.PublicKey().String() - } else { - _, err := wgtypes.ParseKey(client.PublicKey) - if err != nil { - log.Error("Cannot verify wireguard public key: ", err) - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify Wireguard public key"}) - } - // check for duplicates - clients, err := db.GetClients(false) - if err != nil { - log.Error("Cannot get clients for duplicate check") - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get clients for duplicate 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"}) - } - } + key, err := wgtypes.GeneratePrivateKey() + if err != nil { + log.Error("Cannot generate wireguard key pair: ", err) + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot generate Wireguard key pair"}) } - if client.PresharedKey == "" { - presharedKey, err := wgtypes.GenerateKey() - if err != nil { - log.Error("Cannot generated preshared key: ", err) - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ - false, "Cannot generate Wireguard preshared key", - }) - } - client.PresharedKey = presharedKey.String() - } else if client.PresharedKey == "-" { - client.PresharedKey = "" - log.Infof("skipped PresharedKey generation for user: %v", client.Name) - } else { - _, err := wgtypes.ParseKey(client.PresharedKey) - if err != nil { - log.Error("Cannot verify wireguard preshared key: ", err) - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify Wireguard preshared key"}) - } + presharedKey, err := wgtypes.GenerateKey() + if err != nil { + log.Error("Cannot generated preshared key: ", err) + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ + false, "Cannot generate Wireguard preshared key", + }) } + + client.PrivateKey = key.String() + client.PublicKey = key.PublicKey().String() + client.PresharedKey = presharedKey.String() client.CreatedAt = time.Now().UTC() client.UpdatedAt = client.CreatedAt // write client to the database - if err := db.SaveClient(client); err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ - false, err.Error(), - }) - } + db.Write("clients", client.ID, client) log.Infof("Created wireguard client: %v", client) return c.JSON(http.StatusOK, client) } } -// EmailClient handler to send the configuration via email -func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailContent string) echo.HandlerFunc { - type clientIdEmailPayload struct { - ID string `json:"id"` - Email string `json:"email"` - } - - return func(c echo.Context) error { - var payload clientIdEmailPayload - c.Bind(&payload) - // 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{ - Enabled: true, - IncludeDNS: true, - IncludeMTU: true, - } - clientData, err := db.GetClientByID(payload.ID, qrCodeSettings) - 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) - - cfgAtt := emailer.Attachment{Name: "wg0.conf", Data: []byte(config)} - var attachments []emailer.Attachment - if clientData.Client.PrivateKey != "" { - qrdata, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(clientData.QRCode, "data:image/png;base64,")) - if err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "decoding: " + err.Error()}) - } - qrAtt := emailer.Attachment{Name: "wg.png", Data: qrdata} - attachments = []emailer.Attachment{cfgAtt, qrAtt} - } else { - attachments = []emailer.Attachment{cfgAtt} - } - err = mailer.Send( - clientData.Client.Name, - payload.Email, - emailSubject, - emailContent, - attachments, - ) - - if err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) - } - - return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Email sent successfully"}) - } -} - -// 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 -func UpdateClient(db store.IStore) echo.HandlerFunc { +func UpdateClient() echo.HandlerFunc { return func(c echo.Context) error { - var _client model.Client - c.Bind(&_client) + // access validation + validSession(c) - if _, err := xid.FromString(_client.ID); err != nil { - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"}) + _client := new(model.Client) + c.Bind(_client) + + db, err := util.DBConn() + if err != nil { + log.Error("Cannot initialize database: ", err) + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot access database"}) } // validate client existence - clientData, err := db.GetClientByID(_client.ID, model.QRCodeSettings{Enabled: false}) - if err != nil { + client := model.Client{} + if err := db.Read("clients", _client.ID, &client); err != nil { 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() - if err != nil { + // read server information + serverInterface := model.ServerInterface{} + if err := db.Read("server", "interfaces", &serverInterface); err != nil { + log.Error("Cannot fetch server interface config from database: ", err) return c.JSON(http.StatusBadRequest, jsonHTTPResponse{ false, fmt.Sprintf("Cannot fetch server config: %s", err), }) } - client := *clientData.Client + // validate the input Allocation IPs allocatedIPs, err := util.GetAllocatedIPs(client.ID) - check, err := util.ValidateIPAllocation(server.Interface.Addresses, allocatedIPs, _client.AllocatedIPs) + check, err := util.ValidateIPAllocation(serverInterface.Addresses, allocatedIPs, _client.AllocatedIPs) if !check { return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, fmt.Sprintf("%s", err)}) } @@ -664,68 +237,16 @@ func UpdateClient(db store.IStore) echo.HandlerFunc { return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Allowed IPs must be in CIDR format"}) } - if util.ValidateExtraAllowedIPs(_client.ExtraAllowedIPs) == false { - log.Warnf("Invalid Allowed IPs input from user: %v", _client.ExtraAllowedIPs) - 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 client.Name = _client.Name client.Email = _client.Email - client.TgUserid = _client.TgUserid client.Enabled = _client.Enabled - client.UseServerDNS = _client.UseServerDNS client.AllocatedIPs = _client.AllocatedIPs client.AllowedIPs = _client.AllowedIPs - client.ExtraAllowedIPs = _client.ExtraAllowedIPs - client.Endpoint = _client.Endpoint - client.PublicKey = _client.PublicKey - client.PresharedKey = _client.PresharedKey client.UpdatedAt = time.Now().UTC() - client.AdditionalNotes = strings.ReplaceAll(strings.Trim(_client.AdditionalNotes, "\r\n"), "\r\n", "\n") // write to the database - if err := db.SaveClient(client); err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) - } + db.Write("clients", client.ID, &client) log.Infof("Updated client information successfully => %v", client) return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated client successfully"}) @@ -733,8 +254,11 @@ func UpdateClient(db store.IStore) echo.HandlerFunc { } // SetClientStatus handler to enable / disable a client -func SetClientStatus(db store.IStore) echo.HandlerFunc { +func SetClientStatus() echo.HandlerFunc { return func(c echo.Context) error { + // access validation + validSession(c) + data := make(map[string]interface{}) err := json.NewDecoder(c.Request().Body).Decode(&data) @@ -745,21 +269,19 @@ func SetClientStatus(db store.IStore) echo.HandlerFunc { clientID := data["id"].(string) 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}) + db, err := util.DBConn() if err != nil { - return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) + log.Error("Cannot initialize database: ", err) + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot access database"}) } - client := *clientData.Client + client := model.Client{} + if err := db.Read("clients", clientID, &client); err != nil { + log.Error("Cannot get client from database: ", err) + } client.Enabled = status - if err := db.SaveClient(client); err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) - } + db.Write("clients", clientID, &client) log.Infof("Changed client %s enabled status to %v", client.ID, status) return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Changed client status successfully"}) @@ -767,56 +289,50 @@ func SetClientStatus(db store.IStore) echo.HandlerFunc { } // DownloadClient handler -func DownloadClient(db store.IStore) echo.HandlerFunc { +func DownloadClient() echo.HandlerFunc { return func(c echo.Context) error { clientID := c.QueryParam("clientid") if clientID == "" { 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 := util.GetClientByID(clientID, false) if err != nil { log.Errorf("Cannot generate client id %s config file for downloading: %v", clientID, err) return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) } // build config - server, err := db.GetServer() - if err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) - } - globalSettings, err := db.GetGlobalSettings() - if err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) - } + server, _ := util.GetServer() + globalSettings, _ := util.GetGlobalSettings() config := util.BuildClientConfig(*clientData.Client, server, globalSettings) // create io reader from string reader := strings.NewReader(config) // set response header for downloading - c.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%s.conf", clientData.Client.Name)) - return c.Stream(http.StatusOK, "text/conf", reader) + c.Response().Header().Set(echo.HeaderContentDisposition, "attachment; filename=wg0.conf") + return c.Stream(http.StatusOK, "text/plain", reader) } } // RemoveClient handler -func RemoveClient(db store.IStore) echo.HandlerFunc { +func RemoveClient() echo.HandlerFunc { return func(c echo.Context) error { + // access validation + validSession(c) + client := new(model.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 + db, err := util.DBConn() + if err != nil { + log.Error("Cannot initialize database: ", err) + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot access database"}) } - // delete client from database - - if err := db.DeleteClient(client.ID); err != nil { + if err := db.Delete("clients", client.ID); err != nil { log.Error("Cannot delete wireguard client: ", err) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot delete client from database"}) } @@ -827,15 +343,18 @@ func RemoveClient(db store.IStore) echo.HandlerFunc { } // WireGuardServer handler -func WireGuardServer(db store.IStore) echo.HandlerFunc { +func WireGuardServer() echo.HandlerFunc { return func(c echo.Context) error { - server, err := db.GetServer() + // access validation + validSession(c) + + server, err := util.GetServer() if err != nil { log.Error("Cannot get server config: ", err) } return c.Render(http.StatusOK, "server.html", map[string]interface{}{ - "baseData": model.BaseData{Active: "wg-server", CurrentUser: currentUser(c), Admin: isAdmin(c)}, + "baseData": model.BaseData{Active: "wg-server", CurrentUser: currentUser(c)}, "serverInterface": server.Interface, "serverKeyPair": server.KeyPair, }) @@ -843,10 +362,13 @@ func WireGuardServer(db store.IStore) echo.HandlerFunc { } // WireGuardServerInterfaces handler -func WireGuardServerInterfaces(db store.IStore) echo.HandlerFunc { +func WireGuardServerInterfaces() echo.HandlerFunc { return func(c echo.Context) error { - var serverInterface model.ServerInterface - c.Bind(&serverInterface) + // access validation + validSession(c) + + serverInterface := new(model.ServerInterface) + c.Bind(serverInterface) // validate the input addresses if util.ValidateServerAddresses(serverInterface.Addresses) == false { @@ -857,10 +379,13 @@ func WireGuardServerInterfaces(db store.IStore) echo.HandlerFunc { serverInterface.UpdatedAt = time.Now().UTC() // write config to the database - - if err := db.SaveServerInterface(serverInterface); err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Interface IP address must be in CIDR format"}) + db, err := util.DBConn() + if err != nil { + log.Error("Cannot initialize database: ", err) + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot access database"}) } + + db.Write("server", "interfaces", serverInterface) log.Infof("Updated wireguard server interfaces settings: %v", serverInterface) return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated interface addresses successfully"}) @@ -868,8 +393,11 @@ func WireGuardServerInterfaces(db store.IStore) echo.HandlerFunc { } // WireGuardServerKeyPair handler to generate private and public keys -func WireGuardServerKeyPair(db store.IStore) echo.HandlerFunc { +func WireGuardServerKeyPair() echo.HandlerFunc { return func(c echo.Context) error { + // access validation + validSession(c) + // gen Wireguard key pair key, err := wgtypes.GeneratePrivateKey() if err != nil { @@ -877,14 +405,19 @@ func WireGuardServerKeyPair(db store.IStore) echo.HandlerFunc { return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot generate Wireguard key pair"}) } - var serverKeyPair model.ServerKeypair + serverKeyPair := new(model.ServerKeypair) serverKeyPair.PrivateKey = key.String() serverKeyPair.PublicKey = key.PublicKey().String() serverKeyPair.UpdatedAt = time.Now().UTC() - if err := db.SaveServerKeyPair(serverKeyPair); err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot generate Wireguard key pair"}) + // write config to the database + db, err := util.DBConn() + if err != nil { + log.Error("Cannot initialize database: ", err) + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot access database"}) } + + db.Write("server", "keypair", serverKeyPair) log.Infof("Updated wireguard server interfaces settings: %v", serverKeyPair) return c.JSON(http.StatusOK, serverKeyPair) @@ -892,125 +425,31 @@ func WireGuardServerKeyPair(db store.IStore) echo.HandlerFunc { } // GlobalSettings handler -func GlobalSettings(db store.IStore) echo.HandlerFunc { +func GlobalSettings() echo.HandlerFunc { return func(c echo.Context) error { - globalSettings, err := db.GetGlobalSettings() + // access validation + validSession(c) + + globalSettings, err := util.GetGlobalSettings() if err != nil { log.Error("Cannot get global settings: ", err) } return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{ - "baseData": model.BaseData{Active: "global-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, + "baseData": model.BaseData{Active: "global-settings", CurrentUser: currentUser(c)}, "globalSettings": globalSettings, }) } } -// Status handler -func Status(db store.IStore) echo.HandlerFunc { - type PeerVM struct { - Name string - Email string - PublicKey string - ReceivedBytes int64 - TransmitBytes int64 - LastHandshakeTime time.Time - LastHandshakeRel time.Duration - Connected bool - AllocatedIP string - Endpoint string - } - - type DeviceVM struct { - Name string - Peers []PeerVM - } - return func(c echo.Context) error { - wgClient, err := wgctrl.New() - if err != nil { - return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ - "baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, - "error": err.Error(), - "devices": nil, - }) - } - - devices, err := wgClient.Devices() - if err != nil { - return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ - "baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, - "error": err.Error(), - "devices": nil, - }) - } - - devicesVm := make([]DeviceVM, 0, len(devices)) - if len(devices) > 0 { - m := make(map[string]*model.Client) - clients, err := db.GetClients(false) - if err != nil { - return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ - "baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, - "error": err.Error(), - "devices": nil, - }) - } - for i := range clients { - if clients[i].Client != nil { - m[clients[i].Client.PublicKey] = clients[i].Client - } - } - - conv := map[bool]int{true: 1, false: 0} - for i := range devices { - devVm := DeviceVM{Name: devices[i].Name} - for j := range devices[i].Peers { - var allocatedIPs string - for _, ip := range devices[i].Peers[j].AllowedIPs { - if len(allocatedIPs) > 0 { - allocatedIPs += "
" - } - allocatedIPs += ip.String() - } - pVm := PeerVM{ - PublicKey: devices[i].Peers[j].PublicKey.String(), - ReceivedBytes: devices[i].Peers[j].ReceiveBytes, - TransmitBytes: devices[i].Peers[j].TransmitBytes, - LastHandshakeTime: devices[i].Peers[j].LastHandshakeTime, - LastHandshakeRel: time.Since(devices[i].Peers[j].LastHandshakeTime), - AllocatedIP: allocatedIPs, - } - pVm.Connected = pVm.LastHandshakeRel.Minutes() < 3. - - if isAdmin(c) { - pVm.Endpoint = devices[i].Peers[j].Endpoint.String() - } - - if _client, ok := m[pVm.PublicKey]; ok { - pVm.Name = _client.Name - pVm.Email = _client.Email - } - devVm.Peers = append(devVm.Peers, pVm) - } - sort.SliceStable(devVm.Peers, func(i, j int) bool { return devVm.Peers[i].Name < devVm.Peers[j].Name }) - sort.SliceStable(devVm.Peers, func(i, j int) bool { return conv[devVm.Peers[i].Connected] > conv[devVm.Peers[j].Connected] }) - devicesVm = append(devicesVm, devVm) - } - } - - return c.Render(http.StatusOK, "status.html", map[string]interface{}{ - "baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, - "devices": devicesVm, - "error": "", - }) - } -} - // GlobalSettingSubmit handler to update the global settings -func GlobalSettingSubmit(db store.IStore) echo.HandlerFunc { +func GlobalSettingSubmit() echo.HandlerFunc { return func(c echo.Context) error { - var globalSettings model.GlobalSetting - c.Bind(&globalSettings) + // access validation + validSession(c) + + globalSettings := new(model.GlobalSetting) + c.Bind(globalSettings) // validate the input dns server list if util.ValidateIPAddressList(globalSettings.DNSServers) == false { @@ -1021,10 +460,13 @@ func GlobalSettingSubmit(db store.IStore) echo.HandlerFunc { globalSettings.UpdatedAt = time.Now().UTC() // write config to the database - if err := db.SaveGlobalSettings(globalSettings); err != nil { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot generate Wireguard key pair"}) + db, err := util.DBConn() + if err != nil { + log.Error("Cannot initialize database: ", err) + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot access database"}) } + db.Write("server", "global_settings", globalSettings) log.Infof("Updated global settings: %v", globalSettings) return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated global settings successfully"}) @@ -1034,6 +476,9 @@ func GlobalSettingSubmit(db store.IStore) echo.HandlerFunc { // MachineIPAddresses handler to get local interface ip addresses func MachineIPAddresses() echo.HandlerFunc { return func(c echo.Context) error { + // access validation + validSession(c) + // get private ip addresses interfaceList, err := util.GetInterfaceIPs() if err != nil { @@ -1054,20 +499,15 @@ 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 -func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { +func SuggestIPAllocation() echo.HandlerFunc { return func(c echo.Context) error { - server, err := db.GetServer() + // access validation + validSession(c) + + server, err := util.GetServer() if err != nil { log.Error("Cannot fetch server config from database: ", err) - return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, err.Error()}) } // return the list of suggestedIPs @@ -1081,46 +521,16 @@ func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { false, "Cannot suggest ip allocation: failed to get list of allocated ip addresses", }) } - - sr := c.QueryParam("sr") - searchCIDRList := make([]string, 0) - found := false - - // Use subnet range or default to interface addresses - if util.SubnetRanges[sr] != nil { - for _, cidr := range util.SubnetRanges[sr] { - searchCIDRList = append(searchCIDRList, cidr.String()) - } - } else { - searchCIDRList = append(searchCIDRList, server.Interface.Addresses...) - } - - // Save only unique IPs - ipSet := make(map[string]struct{}) - - for _, cidr := range searchCIDRList { - ip, err := util.GetAvailableIP(cidr, allocatedIPs, server.Interface.Addresses) + for _, cidr := range server.Interface.Addresses { + ip, err := util.GetAvailableIP(cidr, allocatedIPs) if err != nil { 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, ":") { - ipSet[fmt.Sprintf("%s/128", ip)] = struct{}{} - } else { - ipSet[fmt.Sprintf("%s/32", ip)] = struct{}{} - } - } - - if !found { - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ - false, - "Cannot suggest ip allocation: failed to get available ip. Try a different subnet or deallocate some ips.", - }) - } - - for ip := range ipSet { - suggestedIPs = append(suggestedIPs, ip) + suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/32", ip)) } return c.JSON(http.StatusOK, suggestedIPs) @@ -1128,34 +538,31 @@ func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { } // ApplyServerConfig handler to write config file and restart Wireguard server -func ApplyServerConfig(db store.IStore, tmplDir fs.FS) echo.HandlerFunc { +func ApplyServerConfig(tmplBox *rice.Box) echo.HandlerFunc { return func(c echo.Context) error { - server, err := db.GetServer() + // access validation + validSession(c) + + server, err := util.GetServer() if err != nil { log.Error("Cannot get server config: ", err) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get server config"}) } - clients, err := db.GetClients(false) + clients, err := util.GetClients(false) if err != nil { log.Error("Cannot get client config: ", err) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"}) } - users, err := db.GetUsers() - if err != nil { - log.Error("Cannot get users config: ", err) - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get users config"}) - } - - settings, err := db.GetGlobalSettings() + settings, err := util.GetGlobalSettings() if err != nil { log.Error("Cannot get global settings: ", err) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get global settings"}) } // Write config file - err = util.WriteWireGuardServerConfig(tmplDir, server, clients, users, settings) + err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings) if err != nil { log.Error("Cannot apply server config: ", err) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ @@ -1163,34 +570,6 @@ func ApplyServerConfig(db store.IStore, tmplDir fs.FS) echo.HandlerFunc { }) } - err = util.UpdateHashes(db) - if err != nil { - log.Error("Cannot update hashes: ", err) - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ - false, fmt.Sprintf("Cannot update hashes: %v", err), - }) - } - return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Applied server config successfully"}) } } - -// GetHashesChanges handler returns if database hashes have changed -func GetHashesChanges(db store.IStore) echo.HandlerFunc { - return func(c echo.Context) error { - if util.HashesChanged(db) { - return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Hashes changed"}) - } else { - return c.JSON(http.StatusOK, jsonHTTPResponse{false, "Hashes not changed"}) - } - } -} - -// AboutPage handler -func AboutPage() echo.HandlerFunc { - return func(c echo.Context) error { - return c.Render(http.StatusOK, "about.html", map[string]interface{}{ - "baseData": model.BaseData{Active: "about", CurrentUser: currentUser(c), Admin: isAdmin(c)}, - }) - } -} diff --git a/handler/routes_wake_on_lan.go b/handler/routes_wake_on_lan.go deleted file mode 100644 index 1747a1e..0000000 --- a/handler/routes_wake_on_lan.go +++ /dev/null @@ -1,172 +0,0 @@ -package handler - -import ( - "fmt" - "net" - "net/http" - "time" - - "github.com/labstack/echo/v4" - "github.com/labstack/gommon/log" - "github.com/ngoduykhanh/wireguard-ui/model" - "github.com/ngoduykhanh/wireguard-ui/store" - "github.com/sabhiram/go-wol/wol" -) - -type WakeOnLanHostSavePayload struct { - Name string `json:"name"` - MacAddress string `json:"mac_address"` - OldMacAddress string `json:"old_mac_address"` -} - -func createError(c echo.Context, err error, msg string) error { - log.Error(msg, err) - return c.JSON( - http.StatusInternalServerError, - jsonHTTPResponse{ - false, - msg}) -} - -func GetWakeOnLanHosts(db store.IStore) echo.HandlerFunc { - return func(c echo.Context) error { - var err error - - hosts, err := db.GetWakeOnLanHosts() - if err != nil { - return createError(c, err, fmt.Sprintf("wake_on_lan_hosts database error: %s", err)) - } - - err = c.Render(http.StatusOK, "wake_on_lan_hosts.html", map[string]interface{}{ - "baseData": model.BaseData{Active: "wake_on_lan_hosts", CurrentUser: currentUser(c), Admin: isAdmin(c)}, - "hosts": hosts, - "error": "", - }) - if err != nil { - return createError(c, err, fmt.Sprintf("wake_on_lan_hosts.html render error: %s", err)) - } - - return nil - } -} - -func SaveWakeOnLanHost(db store.IStore) echo.HandlerFunc { - return func(c echo.Context) error { - var payload WakeOnLanHostSavePayload - err := c.Bind(&payload) - if err != nil { - log.Error("Wake On Host Save Payload Bind Error: ", err) - return c.JSON(http.StatusInternalServerError, payload) - } - - var host = model.WakeOnLanHost{ - MacAddress: payload.MacAddress, - Name: payload.Name, - } - if len(payload.OldMacAddress) != 0 { // Edit - if payload.OldMacAddress != payload.MacAddress { // modified mac address - oldHost, err := db.GetWakeOnLanHost(payload.OldMacAddress) - if err != nil { - return createError(c, err, fmt.Sprintf("Wake On Host Update Err: %s", err)) - } - - if payload.OldMacAddress != payload.MacAddress { - existHost, _ := db.GetWakeOnLanHost(payload.MacAddress) - if existHost != nil { - return createError(c, nil, "Mac Address already exists.") - } - } - - err = db.DeleteWakeOnHostLanHost(payload.OldMacAddress) - if err != nil { - return createError(c, err, fmt.Sprintf("Wake On Host Update Err: %s", err)) - } - host.LatestUsed = oldHost.LatestUsed - } - err = db.SaveWakeOnLanHost(host) - } else { // new - existHost, _ := db.GetWakeOnLanHost(payload.MacAddress) - if existHost != nil { - return createError(c, nil, "Mac Address already exists.") - } - - err = db.SaveWakeOnLanHost(host) - } - - if err != nil { - return createError(c, err, fmt.Sprintf("Wake On Host Save Error: %s", err)) - } - - return c.JSON(http.StatusOK, host) - } -} - -func DeleteWakeOnHost(db store.IStore) echo.HandlerFunc { - return func(c echo.Context) error { - var macAddress = c.Param("mac_address") - var host, err = db.GetWakeOnLanHost(macAddress) - - if err != nil { - log.Error("Wake On Host Delete Error: ", err) - return createError(c, err, fmt.Sprintf("Wake On Host Delete Error: %s", macAddress)) - } - - err = db.DeleteWakeOnHost(*host) - if err != nil { - return createError(c, err, fmt.Sprintf("Wake On Host Delete Error: %s", macAddress)) - } - - return c.JSON(http.StatusOK, nil) - } -} - -func WakeOnHost(db store.IStore) echo.HandlerFunc { - return func(c echo.Context) error { - macAddress := c.Param("mac_address") - host, err := db.GetWakeOnLanHost(macAddress) - - now := time.Now().UTC() - host.LatestUsed = &now - err = db.SaveWakeOnLanHost(*host) - if err != nil { - return createError(c, err, fmt.Sprintf("Latest Used Update Error: %s", macAddress)) - } - - magicPacket, err := wol.New(macAddress) - if err != nil { - return createError(c, err, fmt.Sprintf("Magic Packet Create Error: %s", macAddress)) - } - - bytes, err := magicPacket.Marshal() - if err != nil { - return createError(c, err, fmt.Sprintf("Magic Packet Bytestream Error: %s", macAddress)) - } - - udpAddr, err := net.ResolveUDPAddr("udp", "255.255.255.255:0") - if err != nil { - return createError(c, err, fmt.Sprintf("ResolveUDPAddr Error: %s", macAddress)) - } - - // Grab a UDP connection to send our packet of bytes. - conn, err := net.DialUDP("udp", nil, udpAddr) - if err != nil { - return err - } - defer func(conn *net.UDPConn) { - err := conn.Close() - if err != nil { - log.Error(err) - } - }(conn) - - n, err := conn.Write(bytes) - if err == nil && n != 102 { - return createError(c, nil, fmt.Sprintf("magic packet sent was %d bytes (expected 102 bytes sent)", n)) - } - if err != nil { - return createError(c, err, fmt.Sprintf("Network Send Error: %s", macAddress)) - } - - return c.JSON(http.StatusOK, host.LatestUsed) - } -} diff --git a/handler/session.go b/handler/session.go index b660d9c..6985327 100644 --- a/handler/session.go +++ b/handler/session.go @@ -3,192 +3,25 @@ package handler import ( "fmt" "net/http" - "time" - "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" "github.com/ngoduykhanh/wireguard-ui/util" ) -func ValidSession(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - if !isValidSession(c) { +// validSession to redirect user to the login page if they are not authenticated or session expired. +func validSession(c echo.Context) { + if !util.DisableLogin { + sess, _ := session.Get("session", c) + cookie, err := c.Cookie("session_token") + if err != nil || sess.Values["session_token"] != cookie.Value { nextURL := c.Request().URL - if nextURL != nil && c.Request().Method == http.MethodGet { - return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath+"/login?next=%s", c.Request().URL)) + if nextURL != nil { + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("/login?next=%s", c.Request().URL)) } else { - return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login") + c.Redirect(http.StatusTemporaryRedirect, "/login") } } - return next(c) - } -} - -// RefreshSession must only be used after ValidSession middleware -// RefreshSession checks if the session is eligible for the refresh, but doesn't check if it's fully valid -func RefreshSession(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - doRefreshSession(c) - return next(c) - } -} - -func NeedsAdmin(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - if !isAdmin(c) { - return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/") - } - return next(c) - } -} - -func isValidSession(c echo.Context) bool { - if util.DisableLogin { - return true - } - sess, _ := session.Get("session", c) - cookie, err := c.Cookie("session_token") - if err != nil || sess.Values["session_token"] != cookie.Value { - return false - } - - // Check time bounds - createdAt := getCreatedAt(sess) - updatedAt := getUpdatedAt(sess) - maxAge := getMaxAge(sess) - // Temporary session is considered valid within 24h if browser is not closed before - // This value is not saved and is used as virtual expiration - if maxAge == 0 { - maxAge = 86400 - } - expiration := updatedAt + int64(maxAge) - now := time.Now().UTC().Unix() - if updatedAt > now || expiration < now || createdAt+util.SessionMaxDuration < now { - return false - } - - // Check if user still exists and unchanged - username := fmt.Sprintf("%s", sess.Values["username"]) - userHash := getUserHash(sess) - if uHash, ok := util.DBUsersToCRC32[username]; !ok || userHash != uHash { - return false - } - - return true -} - -// Refreshes a "remember me" session when the user visits web pages (not API) -// Session must be valid before calling this function -// Refresh is performed at most once per 24h -func doRefreshSession(c echo.Context) { - if util.DisableLogin { - return - } - - sess, _ := session.Get("session", c) - maxAge := getMaxAge(sess) - if maxAge <= 0 { - return - } - - oldCookie, err := c.Cookie("session_token") - if err != nil || sess.Values["session_token"] != oldCookie.Value { - return - } - - // Refresh no sooner than 24h - createdAt := getCreatedAt(sess) - updatedAt := getUpdatedAt(sess) - expiration := updatedAt + int64(getMaxAge(sess)) - now := time.Now().UTC().Unix() - if updatedAt > now || expiration < now || now-updatedAt < 86_400 || createdAt+util.SessionMaxDuration < now { - return - } - - cookiePath := util.GetCookiePath() - - sess.Values["updated_at"] = now - sess.Options = &sessions.Options{ - Path: cookiePath, - MaxAge: maxAge, - HttpOnly: true, - SameSite: http.SameSiteLaxMode, - } - sess.Save(c.Request(), c.Response()) - - cookie := new(http.Cookie) - cookie.Name = "session_token" - cookie.Path = cookiePath - cookie.Value = oldCookie.Value - cookie.MaxAge = maxAge - cookie.HttpOnly = true - cookie.SameSite = http.SameSiteLaxMode - c.SetCookie(cookie) -} - -// Get time in seconds this session is valid without updating -func getMaxAge(sess *sessions.Session) int { - if util.DisableLogin { - return 0 - } - - maxAge := sess.Values["max_age"] - - switch typedMaxAge := maxAge.(type) { - case int: - return typedMaxAge - default: - return 0 - } -} - -// Get a timestamp in seconds of the time the session was created -func getCreatedAt(sess *sessions.Session) int64 { - if util.DisableLogin { - return 0 - } - - createdAt := sess.Values["created_at"] - - switch typedCreatedAt := createdAt.(type) { - case int64: - return typedCreatedAt - default: - return 0 - } -} - -// Get a timestamp in seconds of the last session update -func getUpdatedAt(sess *sessions.Session) int64 { - if util.DisableLogin { - return 0 - } - - lastUpdate := sess.Values["updated_at"] - - switch typedLastUpdate := lastUpdate.(type) { - case int64: - return typedLastUpdate - default: - return 0 - } -} - -// Get CRC32 of a user at the moment of log in -// Any changes to user will result in logout of other (not updated) sessions -func getUserHash(sess *sessions.Session) uint32 { - if util.DisableLogin { - return 0 - } - - userHash := sess.Values["user_hash"] - - switch typedUserHash := userHash.(type) { - case uint32: - return typedUserHash - default: - return 0 } } @@ -203,47 +36,10 @@ func currentUser(c echo.Context) string { return username } -// isAdmin to get user type: admin or manager -func isAdmin(c echo.Context) bool { - if util.DisableLogin { - return true - } - - sess, _ := session.Get("session", c) - admin := fmt.Sprintf("%t", sess.Values["admin"]) - return admin == "true" -} - -func setUser(c echo.Context, username string, admin bool, userCRC32 uint32) { - sess, _ := session.Get("session", c) - sess.Values["username"] = username - sess.Values["user_hash"] = userCRC32 - sess.Values["admin"] = admin - sess.Save(c.Request(), c.Response()) -} - // clearSession to remove current session func clearSession(c echo.Context) { sess, _ := session.Get("session", c) sess.Values["username"] = "" - sess.Values["user_hash"] = 0 - sess.Values["admin"] = false sess.Values["session_token"] = "" - sess.Values["max_age"] = -1 - sess.Options.MaxAge = -1 sess.Save(c.Request(), c.Response()) - - cookiePath := util.GetCookiePath() - - cookie, err := c.Cookie("session_token") - if err != nil { - cookie = new(http.Cookie) - } - - cookie.Name = "session_token" - cookie.Path = cookiePath - cookie.MaxAge = -1 - cookie.HttpOnly = true - cookie.SameSite = http.SameSiteLaxMode - c.SetCookie(cookie) } diff --git a/init.sh b/init.sh deleted file mode 100755 index 08b98e8..0000000 --- a/init.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# extract wg config file path, or use default -conf="$(jq -r .config_file_path db/server/global_settings.json || echo /etc/wireguard/wg0.conf)" - -# manage wireguard stop/start with the container -case $WGUI_MANAGE_START in (1|t|T|true|True|TRUE) - wg-quick up "$conf" - trap 'wg-quick down "$conf"' SIGTERM # catches container stop -esac - -# manage wireguard restarts -case $WGUI_MANAGE_RESTART in (1|t|T|true|True|TRUE) - [[ -f $conf ]] || touch "$conf" # inotifyd needs file to exist - inotifyd - "$conf":w | while read -r event file; do - wg-quick down "$file" - wg-quick up "$file" - done & -esac - - -./wg-ui & -wait $! diff --git a/main.go b/main.go index 1125746..0553272 100644 --- a/main.go +++ b/main.go @@ -1,340 +1,93 @@ package main import ( - "crypto/sha512" - "embed" "flag" "fmt" - "io/fs" - "net" "net/http" - "os" - "strings" - "syscall" "time" + rice "github.com/GeertJohan/go.rice" "github.com/labstack/echo/v4" - "github.com/labstack/gommon/log" - "github.com/ngoduykhanh/wireguard-ui/store" - "github.com/ngoduykhanh/wireguard-ui/telegram" - - "github.com/ngoduykhanh/wireguard-ui/emailer" "github.com/ngoduykhanh/wireguard-ui/handler" "github.com/ngoduykhanh/wireguard-ui/router" - "github.com/ngoduykhanh/wireguard-ui/store/jsondb" "github.com/ngoduykhanh/wireguard-ui/util" ) +// command-line banner information var ( - // command-line banner information appVersion = "development" gitCommit = "N/A" gitRef = "N/A" buildTime = fmt.Sprintf(time.Now().UTC().Format("01-02-2006 15:04:05")) - // configuration variables - flagDisableLogin = false - flagBindAddress = "0.0.0.0:5000" - flagSmtpHostname = "127.0.0.1" - flagSmtpPort = 25 - flagSmtpUsername string - flagSmtpPassword string - flagSmtpAuthType = "NONE" - flagSmtpNoTLSCheck = false - flagSmtpEncryption = "STARTTLS" - flagSmtpHelo = "localhost" - flagSendgridApiKey string - flagEmailFrom string - flagEmailFromName = "WireGuard UI" - flagTelegramToken string - flagTelegramAllowConfRequest = false - flagTelegramFloodWait = 60 - flagSessionSecret = util.RandomString(32) - flagSessionMaxDuration = 90 - flagWgConfTemplate string - flagBasePath string - flagSubnetRanges string ) -const ( - defaultEmailSubject = "Your wireguard configuration" - defaultEmailContent = `Hi,
-

In this email you can find your personal configuration for our wireguard server.

- -

Best

-` -) - -// embed the "templates" directory -// -//go:embed templates/* -var embeddedTemplates embed.FS - -// embed the "assets" directory -// -//go:embed assets/* -var embeddedAssets embed.FS - func init() { - // command-line flags and env variables - flag.BoolVar(&flagDisableLogin, "disable-login", util.LookupEnvOrBool("DISABLE_LOGIN", flagDisableLogin), "Disable authentication on the app. This is potentially dangerous.") - flag.StringVar(&flagBindAddress, "bind-address", util.LookupEnvOrString("BIND_ADDRESS", flagBindAddress), "Address:Port to which the app will be bound.") - flag.StringVar(&flagSmtpHostname, "smtp-hostname", util.LookupEnvOrString("SMTP_HOSTNAME", flagSmtpHostname), "SMTP Hostname") - flag.IntVar(&flagSmtpPort, "smtp-port", util.LookupEnvOrInt("SMTP_PORT", flagSmtpPort), "SMTP Port") - flag.StringVar(&flagSmtpHelo, "smtp-helo", util.LookupEnvOrString("SMTP_HELO", flagSmtpHelo), "SMTP HELO Hostname") - flag.StringVar(&flagSmtpUsername, "smtp-username", util.LookupEnvOrString("SMTP_USERNAME", flagSmtpUsername), "SMTP Username") - flag.BoolVar(&flagSmtpNoTLSCheck, "smtp-no-tls-check", util.LookupEnvOrBool("SMTP_NO_TLS_CHECK", flagSmtpNoTLSCheck), "Disable TLS verification for SMTP. This is potentially dangerous.") - flag.StringVar(&flagSmtpEncryption, "smtp-encryption", util.LookupEnvOrString("SMTP_ENCRYPTION", flagSmtpEncryption), "SMTP Encryption : NONE, SSL, SSLTLS, TLS or STARTTLS (by default)") - flag.StringVar(&flagSmtpAuthType, "smtp-auth-type", util.LookupEnvOrString("SMTP_AUTH_TYPE", flagSmtpAuthType), "SMTP Auth Type : PLAIN, LOGIN or NONE.") - flag.StringVar(&flagEmailFrom, "email-from", util.LookupEnvOrString("EMAIL_FROM_ADDRESS", flagEmailFrom), "'From' email address.") - flag.StringVar(&flagEmailFromName, "email-from-name", util.LookupEnvOrString("EMAIL_FROM_NAME", flagEmailFromName), "'From' email name.") - flag.StringVar(&flagTelegramToken, "telegram-token", util.LookupEnvOrString("TELEGRAM_TOKEN", flagTelegramToken), "Telegram bot token for distributing configs to clients.") - flag.BoolVar(&flagTelegramAllowConfRequest, "telegram-allow-conf-request", util.LookupEnvOrBool("TELEGRAM_ALLOW_CONF_REQUEST", flagTelegramAllowConfRequest), "Allow users to get configs from the bot by sending a message.") - flag.IntVar(&flagTelegramFloodWait, "telegram-flood-wait", util.LookupEnvOrInt("TELEGRAM_FLOOD_WAIT", flagTelegramFloodWait), "Time in minutes before the next conf request is processed.") - flag.StringVar(&flagWgConfTemplate, "wg-conf-template", util.LookupEnvOrString("WG_CONF_TEMPLATE", flagWgConfTemplate), "Path to custom wg.conf template.") - flag.StringVar(&flagBasePath, "base-path", util.LookupEnvOrString("BASE_PATH", flagBasePath), "The base path of the URL") - flag.StringVar(&flagSubnetRanges, "subnet-ranges", util.LookupEnvOrString("SUBNET_RANGES", flagSubnetRanges), "IP ranges to choose from when assigning an IP for a client.") - flag.IntVar(&flagSessionMaxDuration, "session-max-duration", util.LookupEnvOrInt("SESSION_MAX_DURATION", flagSessionMaxDuration), "Max time in days a remembered session is refreshed and valid.") - - var ( - smtpPasswordLookup = util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword) - sendgridApiKeyLookup = util.LookupEnvOrString("SENDGRID_API_KEY", flagSendgridApiKey) - sessionSecretLookup = util.LookupEnvOrString("SESSION_SECRET", flagSessionSecret) - ) - - // check empty smtpPassword env var - if smtpPasswordLookup != "" { - flag.StringVar(&flagSmtpPassword, "smtp-password", smtpPasswordLookup, "SMTP Password") - } else { - flag.StringVar(&flagSmtpPassword, "smtp-password", util.LookupEnvOrFile("SMTP_PASSWORD_FILE", flagSmtpPassword), "SMTP Password File") - } - - // check empty sendgridApiKey env var - if sendgridApiKeyLookup != "" { - flag.StringVar(&flagSendgridApiKey, "sendgrid-api-key", sendgridApiKeyLookup, "Your sendgrid api key.") - } else { - flag.StringVar(&flagSendgridApiKey, "sendgrid-api-key", util.LookupEnvOrFile("SENDGRID_API_KEY_FILE", flagSendgridApiKey), "File containing your sendgrid api key.") - } - - // check empty sessionSecret env var - if sessionSecretLookup != "" { - flag.StringVar(&flagSessionSecret, "session-secret", sessionSecretLookup, "The key used to encrypt session cookies.") - } else { - flag.StringVar(&flagSessionSecret, "session-secret", util.LookupEnvOrFile("SESSION_SECRET_FILE", flagSessionSecret), "File containing the key used to encrypt session cookies.") - } - + // command-line flags + flagDisableLogin := flag.Bool("disable-login", false, "Disable login page. Turn off authentication.") + flagBindAddress := flag.String("bind-address", "0.0.0.0:5000", "Address:Port to which the app will be bound.") flag.Parse() // update runtime config - util.DisableLogin = flagDisableLogin - util.BindAddress = flagBindAddress - util.SmtpHostname = flagSmtpHostname - util.SmtpPort = flagSmtpPort - util.SmtpHelo = flagSmtpHelo - util.SmtpUsername = flagSmtpUsername - util.SmtpPassword = flagSmtpPassword - util.SmtpAuthType = flagSmtpAuthType - util.SmtpNoTLSCheck = flagSmtpNoTLSCheck - util.SmtpEncryption = flagSmtpEncryption - util.SendgridApiKey = flagSendgridApiKey - util.EmailFrom = flagEmailFrom - util.EmailFromName = flagEmailFromName - util.SessionSecret = sha512.Sum512([]byte(flagSessionSecret)) - util.SessionMaxDuration = int64(flagSessionMaxDuration) * 86_400 // Store in seconds - util.WgConfTemplate = flagWgConfTemplate - util.BasePath = util.ParseBasePath(flagBasePath) - util.SubnetRanges = util.ParseSubnetRanges(flagSubnetRanges) + util.DisableLogin = *flagDisableLogin + util.BindAddress = *flagBindAddress - lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")) + // print app information + fmt.Println("Wireguard UI") + fmt.Println("App Version\t:", appVersion) + fmt.Println("Git Commit\t:", gitCommit) + fmt.Println("Git Ref\t\t:", gitRef) + fmt.Println("Build Time\t:", buildTime) + fmt.Println("Git Repo\t:", "https://github.com/ngoduykhanh/wireguard-ui") + fmt.Println("Authentication\t:", !util.DisableLogin) + fmt.Println("Bind address\t:", util.BindAddress) - telegram.Token = flagTelegramToken - telegram.AllowConfRequest = flagTelegramAllowConfRequest - telegram.FloodWait = flagTelegramFloodWait - telegram.LogLevel = lvl - - // print only if log level is INFO or lower - if lvl <= log.INFO { - // print app information - fmt.Println("Wireguard UI") - fmt.Println("App Version\t:", appVersion) - fmt.Println("Git Commit\t:", gitCommit) - fmt.Println("Git Ref\t\t:", gitRef) - fmt.Println("Build Time\t:", buildTime) - fmt.Println("Git Repo\t:", "https://github.com/ngoduykhanh/wireguard-ui") - fmt.Println("Authentication\t:", !util.DisableLogin) - fmt.Println("Bind address\t:", util.BindAddress) - //fmt.Println("Sendgrid key\t:", util.SendgridApiKey) - fmt.Println("Email from\t:", util.EmailFrom) - fmt.Println("Email from name\t:", util.EmailFromName) - //fmt.Println("Session secret\t:", util.SessionSecret) - fmt.Println("Custom wg.conf\t:", util.WgConfTemplate) - fmt.Println("Base path\t:", util.BasePath+"/") - fmt.Println("Subnet ranges\t:", util.GetSubnetRangesString()) + // initialize DB + err := util.InitDB() + if err != nil { + fmt.Print("Cannot init database: ", err) } } func main() { - db, err := jsondb.New("./db") - if err != nil { - panic(err) - } - if err := db.Init(); err != nil { - panic(err) - } // set app extra data - extraData := make(map[string]interface{}) + extraData := make(map[string]string) extraData["appVersion"] = appVersion - extraData["gitCommit"] = gitCommit - extraData["basePath"] = util.BasePath - extraData["loginDisabled"] = flagDisableLogin - // strip the "templates/" prefix from the embedded directory so files can be read by their direct name (e.g. - // "base.html" instead of "templates/base.html") - tmplDir, _ := fs.Sub(fs.FS(embeddedTemplates), "templates") + // create rice box for embedded template + tmplBox := rice.MustFindBox("templates") - // create the wireguard config on start, if it doesn't exist - 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()) - } + // rice file server for assets. "assets" is the folder where the files come from. + assetHandler := http.FileServer(rice.MustFindBox("assets").HTTPBox()) // register routes - app := router.New(tmplDir, extraData, util.SessionSecret) + app := router.New(tmplBox, extraData) - app.GET(util.BasePath, handler.WireGuardClients(db), handler.ValidSession, handler.RefreshSession) - - // Important: Make sure that all non-GET routes check the request content type using handler.ContentTypeJson to - // mitigate CSRF attacks. This is effective, because browsers don't allow setting the Content-Type header on - // cross-origin requests. + app.GET("/", handler.WireGuardClients()) if !util.DisableLogin { - app.GET(util.BasePath+"/login", handler.LoginPage()) - app.POST(util.BasePath+"/login", handler.Login(db), handler.ContentTypeJson) - app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession) - app.GET(util.BasePath+"/profile", handler.LoadProfile(), handler.ValidSession, handler.RefreshSession) - app.GET(util.BasePath+"/users-settings", handler.UsersSettings(), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin) - app.POST(util.BasePath+"/update-user", handler.UpdateUser(db), handler.ValidSession, handler.ContentTypeJson) - app.POST(util.BasePath+"/create-user", handler.CreateUser(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) - app.POST(util.BasePath+"/remove-user", handler.RemoveUser(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) - app.GET(util.BasePath+"/get-users", handler.GetUsers(db), handler.ValidSession, handler.NeedsAdmin) - app.GET(util.BasePath+"/api/user/:username", handler.GetUser(db), handler.ValidSession) + app.GET("/login", handler.LoginPage()) + app.POST("/login", handler.Login()) } - var sendmail emailer.Emailer - if util.SendgridApiKey != "" { - sendmail = emailer.NewSendgridApiMail(util.SendgridApiKey, util.EmailFromName, util.EmailFrom) - } else { - sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpHelo, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption) - } + app.GET("/logout", handler.Logout()) + app.POST("/new-client", handler.NewClient()) + app.POST("/update-client", handler.UpdateClient()) + app.POST("/client/set-status", handler.SetClientStatus()) + app.POST("/remove-client", handler.RemoveClient()) + app.GET("/download", handler.DownloadClient()) + app.GET("/wg-server", handler.WireGuardServer()) + app.POST("wg-server/interfaces", handler.WireGuardServerInterfaces()) + app.POST("wg-server/keypair", handler.WireGuardServerKeyPair()) + app.GET("/global-settings", handler.GlobalSettings()) + app.POST("/global-settings", handler.GlobalSettingSubmit()) + app.GET("/api/clients", handler.GetClients()) + app.GET("/api/client/:id", handler.GetClient()) + app.GET("/api/machine-ips", handler.MachineIPAddresses()) + app.GET("/api/suggest-client-ips", handler.SuggestIPAllocation()) + app.GET("/api/apply-wg-config", handler.ApplyServerConfig(tmplBox)) - app.GET(util.BasePath+"/test-hash", handler.GetHashesChanges(db), handler.ValidSession) - app.GET(util.BasePath+"/about", handler.AboutPage()) - app.GET(util.BasePath+"/_health", handler.Health()) - app.GET(util.BasePath+"/favicon", handler.Favicon()) - app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson) - app.POST(util.BasePath+"/update-client", handler.UpdateClient(db), handler.ValidSession, handler.ContentTypeJson) - app.POST(util.BasePath+"/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession, handler.ContentTypeJson) - app.POST(util.BasePath+"/send-telegram-client", handler.SendTelegramClient(db), handler.ValidSession, handler.ContentTypeJson) - app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson) - app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson) - app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession) - app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin) - app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) - app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) - app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin) - app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) - app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession, handler.RefreshSession) - app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession) - app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession) - app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession) - app.GET(util.BasePath+"/api/subnet-ranges", handler.GetOrderedSubnetRanges(), handler.ValidSession) - app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession) - app.POST(util.BasePath+"/api/apply-wg-config", handler.ApplyServerConfig(db, tmplDir), handler.ValidSession, handler.ContentTypeJson) - app.GET(util.BasePath+"/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession, handler.RefreshSession) - app.POST(util.BasePath+"/wake_on_lan_host", handler.SaveWakeOnLanHost(db), handler.ValidSession, handler.ContentTypeJson) - app.DELETE(util.BasePath+"/wake_on_lan_host/:mac_address", handler.DeleteWakeOnHost(db), handler.ValidSession, handler.ContentTypeJson) - app.PUT(util.BasePath+"/wake_on_lan_host/:mac_address", handler.WakeOnHost(db), handler.ValidSession, handler.ContentTypeJson) + // servers other static files + app.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler))) - // strip the "assets/" prefix from the embedded directory so files can be called directly without the "assets/" - // prefix - assetsDir, _ := fs.Sub(fs.FS(embeddedAssets), "assets") - assetHandler := http.FileServer(http.FS(assetsDir)) - // serves other static files - app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler))) - - initDeps := telegram.TgBotInitDependencies{ - DB: db, - SendRequestedConfigsToTelegram: util.SendRequestedConfigsToTelegram, - } - - initTelegram(initDeps) - - if strings.HasPrefix(util.BindAddress, "unix://") { - // Listen on unix domain socket. - // https://github.com/labstack/echo/issues/830 - err := syscall.Unlink(util.BindAddress[6:]) - if err != nil { - app.Logger.Fatalf("Cannot unlink unix socket: Error: %v", err) - } - l, err := net.Listen("unix", util.BindAddress[6:]) - if err != nil { - app.Logger.Fatalf("Cannot create unix socket. Error: %v", err) - } - app.Listener = l - app.Logger.Fatal(app.Start("")) - } else { - // Listen on TCP socket - app.Logger.Fatal(app.Start(util.BindAddress)) - } -} - -func initServerConfig(db store.IStore, tmplDir fs.FS) { - settings, err := db.GetGlobalSettings() - if err != nil { - log.Fatalf("Cannot get global settings: %v", err) - } - - if _, err := os.Stat(settings.ConfigFilePath); err == nil { - // file exists, don't overwrite it implicitly - return - } - - server, err := db.GetServer() - if err != nil { - log.Fatalf("Cannot get server config: %v", err) - } - - clients, err := db.GetClients(false) - if err != nil { - log.Fatalf("Cannot get client config: %v", err) - } - - users, err := db.GetUsers() - if err != nil { - log.Fatalf("Cannot get user config: %v", err) - } - - // write config file - err = util.WriteWireGuardServerConfig(tmplDir, server, clients, users, settings) - if err != nil { - log.Fatalf("Cannot create server config: %v", err) - } -} - -func initTelegram(initDeps telegram.TgBotInitDependencies) { - go func() { - for { - err := telegram.Start(initDeps) - if err == nil { - break - } - } - }() + app.Logger.Fatal(app.Start(util.BindAddress)) } diff --git a/model/client.go b/model/client.go index d835124..913d5f1 100644 --- a/model/client.go +++ b/model/client.go @@ -6,23 +6,17 @@ import ( // Client model type Client struct { - ID string `json:"id"` - PrivateKey string `json:"private_key"` - PublicKey string `json:"public_key"` - PresharedKey string `json:"preshared_key"` - Name string `json:"name"` - TgUserid string `json:"telegram_userid"` - Email string `json:"email"` - SubnetRanges []string `json:"subnet_ranges,omitempty"` - AllocatedIPs []string `json:"allocated_ips"` - AllowedIPs []string `json:"allowed_ips"` - ExtraAllowedIPs []string `json:"extra_allowed_ips"` - Endpoint string `json:"endpoint"` - AdditionalNotes string `json:"additional_notes"` - UseServerDNS bool `json:"use_server_dns"` - Enabled bool `json:"enabled"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + PrivateKey string `json:"private_key"` + PublicKey string `json:"public_key"` + PresharedKey string `json:"preshared_key"` + Name string `json:"name"` + Email string `json:"email"` + AllocatedIPs []string `json:"allocated_ips"` + AllowedIPs []string `json:"allowed_ips"` + Enabled bool `json:"enabled"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // ClientData includes the Client and extra data @@ -30,9 +24,3 @@ type ClientData struct { Client *Client QRCode string } - -type QRCodeSettings struct { - Enabled bool - IncludeDNS bool - IncludeMTU bool -} diff --git a/model/client_defaults.go b/model/client_defaults.go deleted file mode 100644 index 615ebed..0000000 --- a/model/client_defaults.go +++ /dev/null @@ -1,9 +0,0 @@ -package model - -// ClientDefaults Defaults for creation of new clients used in the templates -type ClientDefaults struct { - AllowedIps []string - ExtraAllowedIps []string - UseServerDNS bool - EnableAfterCreation bool -} diff --git a/model/misc.go b/model/misc.go index dc95f15..12d6906 100644 --- a/model/misc.go +++ b/model/misc.go @@ -10,11 +10,4 @@ type Interface struct { type BaseData struct { Active string CurrentUser string - Admin bool -} - -// ClientServerHashes struct, to save hashes to detect changes -type ClientServerHashes struct { - Client string `json:"client"` - Server string `json:"server"` } diff --git a/model/server.go b/model/server.go index 0aa804f..0784eea 100644 --- a/model/server.go +++ b/model/server.go @@ -23,6 +23,5 @@ type ServerInterface struct { ListenPort int `json:"listen_port,string"` // ,string to get listen_port string input as int UpdatedAt time.Time `json:"updated_at"` PostUp string `json:"post_up"` - PreDown string `json:"pre_down"` PostDown string `json:"post_down"` } diff --git a/model/setting.go b/model/setting.go index c9e152c..a316172 100644 --- a/model/setting.go +++ b/model/setting.go @@ -10,8 +10,6 @@ type GlobalSetting struct { DNSServers []string `json:"dns_servers"` MTU int `json:"mtu,string"` PersistentKeepalive int `json:"persistent_keepalive,string"` - FirewallMark string `json:"firewall_mark"` - Table string `json:"table"` ConfigFilePath string `json:"config_file_path"` UpdatedAt time.Time `json:"updated_at"` } diff --git a/model/user.go b/model/user.go index 71f4d13..24a7e53 100644 --- a/model/user.go +++ b/model/user.go @@ -4,7 +4,4 @@ package model type User struct { Username string `json:"username"` Password string `json:"password"` - // PasswordHash takes precedence over Password. - PasswordHash string `json:"password_hash"` - Admin bool `json:"admin"` } diff --git a/model/wake_on_lan_host.go b/model/wake_on_lan_host.go deleted file mode 100644 index 73966f0..0000000 --- a/model/wake_on_lan_host.go +++ /dev/null @@ -1,31 +0,0 @@ -package model - -import ( - "errors" - "net" - "strings" - "time" -) - -type WakeOnLanHost struct { - MacAddress string `json:"MacAddress"` - Name string `json:"Name"` - LatestUsed *time.Time `json:"LatestUsed"` -} - -func (host WakeOnLanHost) ResolveResourceName() (string, error) { - resourceName := strings.Trim(host.MacAddress, " \t\r\n\000") - if len(resourceName) == 0 { - return "", errors.New("mac Address is Empty") - } - resourceName = strings.ToUpper(resourceName) - resourceName = strings.ReplaceAll(resourceName, ":", "-") - - if _, err := net.ParseMAC(resourceName); err != nil { - return "", errors.New("invalid mac address") - } - - return resourceName, nil -} - -const WakeOnLanHostCollectionName = "wake_on_lan_hosts" diff --git a/prepare_assets.sh b/prepare_assets.sh index 66a66b3..31dd1bb 100755 --- a/prepare_assets.sh +++ b/prepare_assets.sh @@ -4,9 +4,7 @@ set -e DIR=$(dirname "$0") # install node modules -YARN=yarn -[ -x /usr/bin/lsb_release ] && [ -n "`lsb_release -i | grep Debian`" ] && YARN=yarnpkg -$YARN install --pure-lockfile --production +yarn install --pure-lockfile --production # Copy admin-lte dist mkdir -p "${DIR}/assets/dist/js" "${DIR}/assets/dist/css" && \ diff --git a/router/router.go b/router/router.go index 59d352e..2bd634e 100644 --- a/router/router.go +++ b/router/router.go @@ -3,23 +3,21 @@ package router import ( "errors" "io" - "io/fs" "reflect" - "strings" "text/template" + "github.com/GeertJohan/go.rice" "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/labstack/gommon/log" - "github.com/ngoduykhanh/wireguard-ui/util" ) // TemplateRegistry is a custom html/template renderer for Echo framework type TemplateRegistry struct { templates map[string]*template.Template - extraData map[string]interface{} + extraData map[string]string } // Render e.Renderer interface @@ -35,8 +33,6 @@ func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c for k, v := range t.extraData { data.(map[string]interface{})[k] = v } - - data.(map[string]interface{})["client_defaults"] = util.ClientDefaultsFromEnv() } // login page does not need the base layout @@ -48,106 +44,52 @@ func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c } // New function -func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo.Echo { +func New(tmplBox *rice.Box, extraData map[string]string) *echo.Echo { e := echo.New() - - 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)) + e.Use(session.Middleware(sessions.NewCookieStore([]byte("secret")))) // read html template file to string - tmplBaseString, err := util.StringFromEmbedFile(tmplDir, "base.html") + tmplBaseString, err := tmplBox.String("base.html") if err != nil { log.Fatal(err) } - tmplLoginString, err := util.StringFromEmbedFile(tmplDir, "login.html") + tmplLoginString, err := tmplBox.String("login.html") if err != nil { log.Fatal(err) } - tmplProfileString, err := util.StringFromEmbedFile(tmplDir, "profile.html") + tmplClientsString, err := tmplBox.String("clients.html") if err != nil { log.Fatal(err) } - tmplClientsString, err := util.StringFromEmbedFile(tmplDir, "clients.html") + tmplServerString, err := tmplBox.String("server.html") if err != nil { log.Fatal(err) } - tmplServerString, err := util.StringFromEmbedFile(tmplDir, "server.html") - if err != nil { - log.Fatal(err) - } - - tmplGlobalSettingsString, err := util.StringFromEmbedFile(tmplDir, "global_settings.html") - if err != nil { - log.Fatal(err) - } - - tmplUsersSettingsString, err := util.StringFromEmbedFile(tmplDir, "users_settings.html") - if err != nil { - log.Fatal(err) - } - - tmplStatusString, err := util.StringFromEmbedFile(tmplDir, "status.html") - if err != nil { - log.Fatal(err) - } - - tmplWakeOnLanHostsString, err := util.StringFromEmbedFile(tmplDir, "wake_on_lan_hosts.html") - if err != nil { - log.Fatal(err) - } - - aboutPageString, err := util.StringFromEmbedFile(tmplDir, "about.html") + tmplGlobalSettingsString, err := tmplBox.String("global_settings.html") if err != nil { log.Fatal(err) } // create template list - funcs := template.FuncMap{ - "StringsJoin": strings.Join, - } templates := make(map[string]*template.Template) - templates["login.html"] = template.Must(template.New("login").Funcs(funcs).Parse(tmplLoginString)) - templates["profile.html"] = template.Must(template.New("profile").Funcs(funcs).Parse(tmplBaseString + tmplProfileString)) - templates["clients.html"] = template.Must(template.New("clients").Funcs(funcs).Parse(tmplBaseString + tmplClientsString)) - templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString)) - templates["global_settings.html"] = template.Must(template.New("global_settings").Funcs(funcs).Parse(tmplBaseString + tmplGlobalSettingsString)) - templates["users_settings.html"] = template.Must(template.New("users_settings").Funcs(funcs).Parse(tmplBaseString + tmplUsersSettingsString)) - templates["status.html"] = template.Must(template.New("status").Funcs(funcs).Parse(tmplBaseString + tmplStatusString)) - templates["wake_on_lan_hosts.html"] = template.Must(template.New("wake_on_lan_hosts").Funcs(funcs).Parse(tmplBaseString + tmplWakeOnLanHostsString)) - templates["about.html"] = template.Must(template.New("about").Funcs(funcs).Parse(tmplBaseString + aboutPageString)) + templates["login.html"] = template.Must(template.New("login").Parse(tmplLoginString)) + templates["clients.html"] = template.Must(template.New("clients").Parse(tmplBaseString + tmplClientsString)) + templates["server.html"] = template.Must(template.New("server").Parse(tmplBaseString + tmplServerString)) + templates["global_settings.html"] = template.Must(template.New("global_settings").Parse(tmplBaseString + tmplGlobalSettingsString)) - lvl, err := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")) - if err != nil { - log.Fatal(err) - } - logConfig := middleware.DefaultLoggerConfig - logConfig.Skipper = func(c echo.Context) bool { - resp := c.Response() - if resp.Status >= 500 && lvl > log.ERROR { // do not log if response is 5XX but log level is higher than ERROR - return true - } else if resp.Status >= 400 && lvl > log.WARN { // do not log if response is 4XX but log level is higher than WARN - return true - } else if lvl > log.DEBUG { // do not log if log level is higher than DEBUG - return true - } - return false - } - - e.Logger.SetLevel(lvl) + e.Logger.SetLevel(log.DEBUG) e.Pre(middleware.RemoveTrailingSlash()) - e.Use(middleware.LoggerWithConfig(logConfig)) + e.Use(middleware.Logger()) + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"*"}, + AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization}, + AllowMethods: []string{echo.GET, echo.HEAD, echo.PUT, echo.PATCH, echo.POST, echo.DELETE}, + })) e.HideBanner = true - e.HidePort = lvl > log.INFO // hide the port output if the log level is higher than INFO e.Validator = NewValidator() e.Renderer = &TemplateRegistry{ templates: templates, diff --git a/store/jsondb/jsondb.go b/store/jsondb/jsondb.go deleted file mode 100644 index 1cd0a43..0000000 --- a/store/jsondb/jsondb.go +++ /dev/null @@ -1,410 +0,0 @@ -package jsondb - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "os" - "path" - "strconv" - "time" - - "github.com/sdomino/scribble" - "github.com/skip2/go-qrcode" - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - - "github.com/ngoduykhanh/wireguard-ui/model" - "github.com/ngoduykhanh/wireguard-ui/util" -) - -type JsonDB struct { - conn *scribble.Driver - dbPath string -} - -// New returns a new pointer JsonDB -func New(dbPath string) (*JsonDB, error) { - conn, err := scribble.New(dbPath, nil) - if err != nil { - return nil, err - } - ans := JsonDB{ - conn: conn, - dbPath: dbPath, - } - return &ans, nil -} - -func (o *JsonDB) Init() error { - var clientPath = path.Join(o.dbPath, "clients") - var serverPath = path.Join(o.dbPath, "server") - var userPath = path.Join(o.dbPath, "users") - var wakeOnLanHostsPath = path.Join(o.dbPath, "wake_on_lan_hosts") - var serverInterfacePath = path.Join(serverPath, "interfaces.json") - var serverKeyPairPath = path.Join(serverPath, "keypair.json") - var globalSettingPath = path.Join(serverPath, "global_settings.json") - var hashesPath = path.Join(serverPath, "hashes.json") - - // create directories if they do not exist - if _, err := os.Stat(clientPath); os.IsNotExist(err) { - os.MkdirAll(clientPath, os.ModePerm) - } - if _, err := os.Stat(serverPath); os.IsNotExist(err) { - os.MkdirAll(serverPath, os.ModePerm) - } - if _, err := os.Stat(userPath); os.IsNotExist(err) { - os.MkdirAll(userPath, os.ModePerm) - } - if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) { - os.MkdirAll(wakeOnLanHostsPath, os.ModePerm) - } - - // server's interface - if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) { - serverInterface := new(model.ServerInterface) - serverInterface.Addresses = util.LookupEnvOrStrings(util.ServerAddressesEnvVar, []string{util.DefaultServerAddress}) - serverInterface.ListenPort = util.LookupEnvOrInt(util.ServerListenPortEnvVar, util.DefaultServerPort) - serverInterface.PostUp = util.LookupEnvOrString(util.ServerPostUpScriptEnvVar, "") - serverInterface.PostDown = util.LookupEnvOrString(util.ServerPostDownScriptEnvVar, "") - serverInterface.UpdatedAt = time.Now().UTC() - o.conn.Write("server", "interfaces", serverInterface) - err := util.ManagePerms(serverInterfacePath) - if err != nil { - return err - } - } - - // server's key pair - if _, err := os.Stat(serverKeyPairPath); os.IsNotExist(err) { - key, err := wgtypes.GeneratePrivateKey() - if err != nil { - return scribble.ErrMissingCollection - } - serverKeyPair := new(model.ServerKeypair) - serverKeyPair.PrivateKey = key.String() - serverKeyPair.PublicKey = key.PublicKey().String() - serverKeyPair.UpdatedAt = time.Now().UTC() - o.conn.Write("server", "keypair", serverKeyPair) - err = util.ManagePerms(serverKeyPairPath) - if err != nil { - return err - } - } - - // global settings - if _, err := os.Stat(globalSettingPath); os.IsNotExist(err) { - endpointAddress := util.LookupEnvOrString(util.EndpointAddressEnvVar, "") - if endpointAddress == "" { - // automatically find an external IP address - publicInterface, err := util.GetPublicIP() - if err != nil { - return err - } - endpointAddress = publicInterface.IPAddress - } - - globalSetting := new(model.GlobalSetting) - globalSetting.EndpointAddress = endpointAddress - globalSetting.DNSServers = util.LookupEnvOrStrings(util.DNSEnvVar, []string{util.DefaultDNS}) - globalSetting.MTU = util.LookupEnvOrInt(util.MTUEnvVar, util.DefaultMTU) - globalSetting.PersistentKeepalive = util.LookupEnvOrInt(util.PersistentKeepaliveEnvVar, util.DefaultPersistentKeepalive) - globalSetting.FirewallMark = util.LookupEnvOrString(util.FirewallMarkEnvVar, util.DefaultFirewallMark) - globalSetting.Table = util.LookupEnvOrString(util.TableEnvVar, util.DefaultTable) - globalSetting.ConfigFilePath = util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath) - globalSetting.UpdatedAt = time.Now().UTC() - o.conn.Write("server", "global_settings", globalSetting) - err := util.ManagePerms(globalSettingPath) - if err != nil { - return err - } - } - - // hashes - if _, err := os.Stat(hashesPath); os.IsNotExist(err) { - clientServerHashes := new(model.ClientServerHashes) - clientServerHashes.Client = "none" - clientServerHashes.Server = "none" - o.conn.Write("server", "hashes", clientServerHashes) - err := util.ManagePerms(hashesPath) - if err != nil { - return err - } - } - - // user info - results, err := o.conn.ReadAll("users") - if err != nil || len(results) < 1 { - user := new(model.User) - user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername) - user.Admin = util.DefaultIsAdmin - user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "") - if user.PasswordHash == "" { - user.PasswordHash = util.LookupEnvOrFile(util.PasswordHashFileEnvVar, "") - if user.PasswordHash == "" { - plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword) - if plaintext == util.DefaultPassword { - plaintext = util.LookupEnvOrFile(util.PasswordFileEnvVar, util.DefaultPassword) - } - hash, err := util.HashPassword(plaintext) - if err != nil { - return err - } - user.PasswordHash = hash - } - } - - o.conn.Write("users", user.Username, user) - results, _ = o.conn.ReadAll("users") - err = util.ManagePerms(path.Join(path.Join(o.dbPath, "users"), user.Username+".json")) - if err != nil { - return err - } - } - - // init cache - for _, i := range results { - user := model.User{} - - if err := json.Unmarshal([]byte(i), &user); err == nil { - util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user) - } - } - - clients, err := o.GetClients(false) - if err != nil { - return nil - } - for _, cl := range clients { - client := cl.Client - if client.Enabled && len(client.TgUserid) > 0 { - if userid, err := strconv.ParseInt(client.TgUserid, 10, 64); err == nil { - util.UpdateTgToClientID(userid, client.ID) - } - } - } - - return nil -} - -// GetUsers func to get all users from the database -func (o *JsonDB) GetUsers() ([]model.User, error) { - var users []model.User - results, err := o.conn.ReadAll("users") - if err != nil { - return users, err - } - for _, i := range results { - user := model.User{} - - if err := json.Unmarshal(i, &user); err != nil { - return users, fmt.Errorf("cannot decode user json structure: %v", err) - } - users = append(users, user) - } - return users, err -} - -// GetUserByName func to get single user from the database -func (o *JsonDB) GetUserByName(username string) (model.User, error) { - user := model.User{} - - if err := o.conn.Read("users", username, &user); err != nil { - return user, err - } - - return user, nil -} - -// SaveUser func to save user in the database -func (o *JsonDB) SaveUser(user model.User) error { - userPath := path.Join(path.Join(o.dbPath, "users"), user.Username+".json") - output := o.conn.Write("users", user.Username, user) - err := util.ManagePerms(userPath) - if err != nil { - return err - } - util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user) - return output -} - -// DeleteUser func to remove user from the database -func (o *JsonDB) DeleteUser(username string) error { - delete(util.DBUsersToCRC32, username) - return o.conn.Delete("users", username) -} - -// GetGlobalSettings func to query global settings from the database -func (o *JsonDB) GetGlobalSettings() (model.GlobalSetting, error) { - settings := model.GlobalSetting{} - return settings, o.conn.Read("server", "global_settings", &settings) -} - -// GetServer func to query Server settings from the database -func (o *JsonDB) GetServer() (model.Server, error) { - server := model.Server{} - // read server interface information - serverInterface := model.ServerInterface{} - if err := o.conn.Read("server", "interfaces", &serverInterface); err != nil { - return server, err - } - - // read server key pair information - serverKeyPair := model.ServerKeypair{} - if err := o.conn.Read("server", "keypair", &serverKeyPair); err != nil { - return server, err - } - - // create Server object and return - server.Interface = &serverInterface - server.KeyPair = &serverKeyPair - return server, nil -} - -func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) { - var clients []model.ClientData - - // read all client json files in "clients" directory - records, err := o.conn.ReadAll("clients") - if err != nil { - return clients, err - } - - // build the ClientData list - for _, f := range records { - client := model.Client{} - clientData := model.ClientData{} - - // get client info - if err := json.Unmarshal(f, &client); err != nil { - return clients, fmt.Errorf("cannot decode client json structure: %v", err) - } - - // generate client qrcode image in base64 - if hasQRCode && client.PrivateKey != "" { - server, _ := o.GetServer() - globalSettings, _ := o.GetGlobalSettings() - - png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256) - if err == nil { - clientData.QRCode = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png) - } else { - fmt.Print("Cannot generate QR code: ", err) - } - } - - // create the list of clients and their qrcode data - clientData.Client = &client - clients = append(clients, clientData) - } - - return clients, nil -} - -func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSettings) (model.ClientData, error) { - client := model.Client{} - clientData := model.ClientData{} - - // read client information - if err := o.conn.Read("clients", clientID, &client); err != nil { - return clientData, err - } - - // generate client qrcode image in base64 - if qrCodeSettings.Enabled && client.PrivateKey != "" { - server, _ := o.GetServer() - globalSettings, _ := o.GetGlobalSettings() - client := client - if !qrCodeSettings.IncludeDNS { - globalSettings.DNSServers = []string{} - } - if !qrCodeSettings.IncludeMTU { - globalSettings.MTU = 0 - } - - png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256) - if err == nil { - clientData.QRCode = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png) - } else { - fmt.Print("Cannot generate QR code: ", err) - } - } - - clientData.Client = &client - - return clientData, nil -} - -func (o *JsonDB) SaveClient(client model.Client) error { - clientPath := path.Join(path.Join(o.dbPath, "clients"), client.ID+".json") - output := o.conn.Write("clients", client.ID, client) - if output == nil { - if client.Enabled && len(client.TgUserid) > 0 { - if userid, err := strconv.ParseInt(client.TgUserid, 10, 64); err == nil { - util.UpdateTgToClientID(userid, client.ID) - } - } else { - util.RemoveTgToClientID(client.ID) - } - } else { - util.RemoveTgToClientID(client.ID) - } - err := util.ManagePerms(clientPath) - if err != nil { - return err - } - return output -} - -func (o *JsonDB) DeleteClient(clientID string) error { - util.RemoveTgToClientID(clientID) - return o.conn.Delete("clients", clientID) -} - -func (o *JsonDB) SaveServerInterface(serverInterface model.ServerInterface) error { - serverInterfacePath := path.Join(path.Join(o.dbPath, "server"), "interfaces.json") - output := o.conn.Write("server", "interfaces", serverInterface) - err := util.ManagePerms(serverInterfacePath) - if err != nil { - return err - } - return output -} - -func (o *JsonDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error { - serverKeyPairPath := path.Join(path.Join(o.dbPath, "server"), "keypair.json") - output := o.conn.Write("server", "keypair", serverKeyPair) - err := util.ManagePerms(serverKeyPairPath) - if err != nil { - return err - } - return output -} - -func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error { - globalSettingsPath := path.Join(path.Join(o.dbPath, "server"), "global_settings.json") - output := o.conn.Write("server", "global_settings", globalSettings) - err := util.ManagePerms(globalSettingsPath) - if err != nil { - return err - } - return output -} - -func (o *JsonDB) GetPath() string { - return o.dbPath -} - -func (o *JsonDB) GetHashes() (model.ClientServerHashes, error) { - hashes := model.ClientServerHashes{} - return hashes, o.conn.Read("server", "hashes", &hashes) -} - -func (o *JsonDB) SaveHashes(hashes model.ClientServerHashes) error { - hashesPath := path.Join(path.Join(o.dbPath, "server"), "hashes.json") - output := o.conn.Write("server", "hashes", hashes) - err := util.ManagePerms(hashesPath) - if err != nil { - return err - } - return output -} diff --git a/store/jsondb/jsondb_wake_on_lan.go b/store/jsondb/jsondb_wake_on_lan.go deleted file mode 100644 index d210d61..0000000 --- a/store/jsondb/jsondb_wake_on_lan.go +++ /dev/null @@ -1,88 +0,0 @@ -package jsondb - -import ( - "encoding/json" - "fmt" - "path" - - "github.com/ngoduykhanh/wireguard-ui/model" - "github.com/ngoduykhanh/wireguard-ui/util" -) - -func (o *JsonDB) GetWakeOnLanHosts() ([]model.WakeOnLanHost, error) { - var hosts []model.WakeOnLanHost - - // read all client json file in "hosts" directory - records, err := o.conn.ReadAll(model.WakeOnLanHostCollectionName) - if err != nil { - return hosts, err - } - - // build the ClientData list - for _, f := range records { - host := model.WakeOnLanHost{} - - // get client info - if err := json.Unmarshal(f, &host); err != nil { - return hosts, fmt.Errorf("cannot decode client json structure: %v", err) - } - - // create the list of hosts and their qrcode data - hosts = append(hosts, host) - } - - return hosts, nil -} - -func (o *JsonDB) GetWakeOnLanHost(macAddress string) (*model.WakeOnLanHost, error) { - host := &model.WakeOnLanHost{ - MacAddress: macAddress, - } - resourceName, err := host.ResolveResourceName() - if err != nil { - return nil, err - } - - err = o.conn.Read(model.WakeOnLanHostCollectionName, resourceName, host) - if err != nil { - host = nil - } - return host, err -} - -func (o *JsonDB) DeleteWakeOnHostLanHost(macAddress string) error { - host := &model.WakeOnLanHost{ - MacAddress: macAddress, - } - resourceName, err := host.ResolveResourceName() - if err != nil { - return err - } - - return o.conn.Delete(model.WakeOnLanHostCollectionName, resourceName) -} - -func (o *JsonDB) SaveWakeOnLanHost(host model.WakeOnLanHost) error { - resourceName, err := host.ResolveResourceName() - if err != nil { - return err - } - - wakeOnLanHostPath := path.Join(path.Join(o.dbPath, model.WakeOnLanHostCollectionName), resourceName+".json") - output := o.conn.Write(model.WakeOnLanHostCollectionName, resourceName, host) - err = util.ManagePerms(wakeOnLanHostPath) - if err != nil { - return err - } - - return output -} - -func (o *JsonDB) DeleteWakeOnHost(host model.WakeOnLanHost) error { - resourceName, err := host.ResolveResourceName() - if err != nil { - return err - } - - return o.conn.Delete(model.WakeOnLanHostCollectionName, resourceName) -} diff --git a/store/store.go b/store/store.go deleted file mode 100644 index ef6d723..0000000 --- a/store/store.go +++ /dev/null @@ -1,30 +0,0 @@ -package store - -import ( - "github.com/ngoduykhanh/wireguard-ui/model" -) - -type IStore interface { - Init() error - GetUsers() ([]model.User, error) - GetUserByName(username string) (model.User, error) - SaveUser(user model.User) error - DeleteUser(username string) error - GetGlobalSettings() (model.GlobalSetting, error) - GetServer() (model.Server, error) - GetClients(hasQRCode bool) ([]model.ClientData, error) - GetClientByID(clientID string, qrCode model.QRCodeSettings) (model.ClientData, error) - SaveClient(client model.Client) error - DeleteClient(clientID string) error - SaveServerInterface(serverInterface model.ServerInterface) error - SaveServerKeyPair(serverKeyPair model.ServerKeypair) error - SaveGlobalSettings(globalSettings model.GlobalSetting) error - GetWakeOnLanHosts() ([]model.WakeOnLanHost, error) - GetWakeOnLanHost(macAddress string) (*model.WakeOnLanHost, error) - DeleteWakeOnHostLanHost(macAddress string) error - SaveWakeOnLanHost(host model.WakeOnLanHost) error - DeleteWakeOnHost(host model.WakeOnLanHost) error - GetPath() string - SaveHashes(hashes model.ClientServerHashes) error - GetHashes() (model.ClientServerHashes, error) -} diff --git a/telegram/bot.go b/telegram/bot.go deleted file mode 100644 index 7842f63..0000000 --- a/telegram/bot.go +++ /dev/null @@ -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) - } - } -} diff --git a/templates/about.html b/templates/about.html deleted file mode 100644 index edbeb47..0000000 --- a/templates/about.html +++ /dev/null @@ -1,145 +0,0 @@ -{{ define "title"}} -About -{{ end }} - -{{ define "top_css"}} -{{ end }} - -{{ define "username"}} -{{ .username }} -{{ end }} - -{{ define "page_title"}} -About -{{ end }} - -{{ define "page_content"}} -
-
- -
- -
-
-
-

About Wireguard-UI

-
- -
-
- - -
-{{ if .gitCommit }} -
- - -
-{{ end }} -
- - -
-
- - -
-
- - -
-
- -
- - - -
-
-
- -
-
- Copyright © - - Wireguard UI. - All rights reserved. - -
-
- -
-
- -
-
-{{ end }} - -{{ define "bottom_js"}} - -{{ end }} diff --git a/templates/base.html b/templates/base.html index 3640699..314c200 100644 --- a/templates/base.html +++ b/templates/base.html @@ -8,23 +8,21 @@ {{template "title" .}} - - - + - + - + - + - + - + @@ -46,27 +44,16 @@ - @@ -74,11 +61,11 @@ - {{if .baseData.CurrentUser}} - {{end}}
@@ -88,8 +75,10 @@
- {{if .baseData.CurrentUser}} - - {{if .baseData.Admin}} - Administrator: {{.baseData.CurrentUser}} - {{else}} - Manager: {{.baseData.CurrentUser}} - {{end}} - - {{else}} - Administrator - {{end}} + {{if .baseData.CurrentUser}} {{.baseData.CurrentUser}} {{else}} Administrator {{end}}
@@ -211,88 +146,23 @@
-
- - -
- + -
-
- - -
-
- - + value="0.0.0.0/0">
- - -
-
-
-
- +
-
- Public and Preshared Keys - - - -
- - -
-
- - -
-
-
- Additional configuration - -
- - -
-
- - -
-
- +