Resolve conflict

This commit is contained in:
Khanh Ngo 2023-12-29 10:13:30 +01:00
commit 8cfe9a3d5b
No known key found for this signature in database
GPG key ID: 29077342AA5648F6
25 changed files with 1550 additions and 405 deletions

View file

@ -72,19 +72,19 @@ jobs:
# set up docker and build images # set up docker and build images
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
push: true push: true
context: . context: .

3
.gitignore vendored
View file

@ -21,6 +21,9 @@ node_modules/
.vscode .vscode
.idea .idea
# Vim
.*.sw[op]
# Examples # Examples
examples/docker-compose/config examples/docker-compose/config
examples/docker-compose/db examples/docker-compose/db

67
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,67 @@
# Contributing Guidelines
Thank you for your interest in contributing to my project. Whether it's a bug report, new feature, correction, or additional
documentation, I greatly value feedback and contributions from my community.
Please read through this document before submitting any issues or pull requests to ensure I have all the necessary
information to effectively respond to your bug report or contribution.
## Reporting Bugs/Feature Requests
I welcome you to use the GitHub issue tracker to report bugs or suggest features.
When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
- A reproducible test case or series of steps
- The version of my code being used
- Any modifications you've made relevant to the bug
- Anything unusual about your environment or deployment
## Contributing via Pull Requests
### Discussion of New Features
Before initiating the implementation of a new feature, I encourage contributors to open a discussion by creating a new GitHub issue. This allows me to provide feedback, share insights, and ensure alignment with the project's direction and save your time.
#### Process for Discussing New Features:
1. **Create an Issue:**
- Go to the "Issues" tab in the repository.
- Click on "New Issue."
- Clearly describe the proposed feature, its purpose, and potential benefits.
2. **Engage in Discussion:**
- Respond promptly to comments and feedback from the community.
- Be open to adjusting the feature based on collaborative input.
3. **Consensus Building:**
- Strive to reach a consensus on the proposed feature.
- Ensure alignment with the overall project vision.
### Bug Fixes and Improvements
For bug fixes, documentation improvements, and general enhancements, feel free to submit a pull request directly.
#### Pull Request Guidelines:
1. **Fork the Repository:**
- Fork the repository to your GitHub account.
2. **Create a Branch:**
- Create a new branch for your changes.
3. **Make Changes:**
- Make your changes and ensure they adhere to coding standards.
4. **Submit a Pull Request:**
- Submit a pull request to the main repository.
5. **Engage in Review:**
- Be responsive to feedback and address any requested changes.
6. **Merge Process:**
- Once approved, your changes will be merged into the main branch.
## Licensing
See the [LICENSE](LICENSE) file for my project's licensing.

View file

@ -1,5 +1,5 @@
# Build stage # Build stage
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.17-alpine3.16 as builder FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.21-alpine3.19 AS builder
LABEL maintainer="Khanh Ngo <k@ndk.name>" LABEL maintainer="Khanh Ngo <k@ndk.name>"
ARG BUILDPLATFORM ARG BUILDPLATFORM
@ -56,7 +56,7 @@ RUN cp -r /build/custom/ assets/
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X 'main.appVersion=${APP_VERSION}' -X 'main.buildTime=${BUILD_TIME}' -X 'main.gitCommit=${GIT_COMMIT}'" -a -o wg-ui . RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X 'main.appVersion=${APP_VERSION}' -X 'main.buildTime=${BUILD_TIME}' -X 'main.gitCommit=${GIT_COMMIT}'" -a -o wg-ui .
# Release stage # Release stage
FROM alpine:3.16 FROM alpine:3.19
RUN addgroup -S wgui && \ RUN addgroup -S wgui && \
adduser -S -D -G wgui wgui adduser -S -D -G wgui wgui

View file

@ -36,34 +36,43 @@ docker-compose up
## Environment Variables ## Environment Variables
| Variable | Description | Default | | Variable | Description | Default |
|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------| |-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|
| `BASE_PATH` | Set this variable if you run wireguard-ui under a subpath of your reverse proxy virtual host (e.g. /wireguard)) | N/A | | `BASE_PATH` | Set this variable if you run wireguard-ui under a subpath of your reverse proxy virtual host (e.g. /wireguard) | N/A |
| `BIND_ADDRESS` | The addresses that can access to the web interface and the port | 0.0.0.0:80 | | `BIND_ADDRESS` | The addresses that can access to the web interface and the port, use unix:///abspath/to/file.socket for unix domain socket. | 0.0.0.0:80 |
| `SESSION_SECRET` | The secret key used to encrypt the session cookies. Set this to a random value | N/A | | `SESSION_SECRET` | The secret key used to encrypt the session cookies. Set this to a random value | N/A |
| `WGUI_USERNAME` | The username for the login page. Used for db initialization only | `admin` | | `SESSION_SECRET_FILE` | Optional filepath for the secret key used to encrypt the session cookies. Leave `SESSION_SECRET` blank to take effect | N/A |
| `WGUI_PASSWORD` | The password for the user on the login page. Will be hashed automatically. Used for db initialization only | `admin` | | `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_PASSWORD_HASH` | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only | N/A | | `WGUI_USERNAME` | The username for the login page. Used for db initialization only | `admin` |
| `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings where clients should connect to | Resolved to your public ip address | | `WGUI_PASSWORD` | The password for the user on the login page. Will be hashed automatically. Used for db initialization only | `admin` |
| `WGUI_FAVICON_FILE_PATH` | The file path used as website favicon | Embedded WireGuard logo | | `WGUI_PASSWORD_FILE` | Optional filepath for the user login password. Will be hashed automatically. Used for db initialization only. Leave `WGUI_PASSWORD` blank to take effect | N/A |
| `WGUI_DNS` | The default DNS servers (comma-separated-list) used in the global settings | `1.1.1.1` | | `WGUI_PASSWORD_HASH` | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only | N/A |
| `WGUI_MTU` | The default MTU used in global settings | `1450` | | `WGUI_PASSWORD_HASH_FILE` | Optional filepath for the user login password hash. (alternative to `WGUI_PASSWORD_FILE`). Used for db initialization only. Leave `WGUI_PASSWORD_HASH` blank to take effect | N/A |
| `WGUI_PERSISTENT_KEEPALIVE` | The default persistent keepalive for WireGuard in global settings | `15` | | `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings where clients should connect to. The endpoint can contain a port as well, useful when you are listening internally on the `WGUI_SERVER_LISTEN_PORT` port, but you forward on another port (ex 9000). Ex: myvpn.dyndns.com:9000 | Resolved to your public ip address |
| `WGUI_FIREWALL_MARK` | The default WireGuard firewall mark | `0xca6c` (51820) | | `WGUI_FAVICON_FILE_PATH` | The file path used as website favicon | Embedded WireGuard logo |
| `WGUI_TABLE` | The default WireGuard table value settings | `auto` | | `WGUI_DNS` | The default DNS servers (comma-separated-list) used in the global settings | `1.1.1.1` |
| `WGUI_CONFIG_FILE_PATH` | The default WireGuard config file path used in global settings | `/etc/wireguard/wg0.conf` | | `WGUI_MTU` | The default MTU used in global settings | `1450` |
| `WGUI_LOG_LEVEL` | The default log level. Possible values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF` | `INFO` | | `WGUI_PERSISTENT_KEEPALIVE` | The default persistent keepalive for WireGuard in global settings | `15` |
| `WG_CONF_TEMPLATE` | The custom `wg.conf` config file template. Please refer to our [default template](https://github.com/ngoduykhanh/wireguard-ui/blob/master/templates/wg.conf) | N/A | | `WGUI_FIREWALL_MARK` | The default WireGuard firewall mark | `0xca6c` (51820) |
| `EMAIL_FROM_ADDRESS` | The sender email address | N/A | | `WGUI_TABLE` | The default WireGuard table value settings | `auto` |
| `EMAIL_FROM_NAME` | The sender name | `WireGuard UI` | | `WGUI_CONFIG_FILE_PATH` | The default WireGuard config file path used in global settings | `/etc/wireguard/wg0.conf` |
| `SENDGRID_API_KEY` | The SendGrid api key | N/A | | `WGUI_LOG_LEVEL` | The default log level. Possible values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF` | `INFO` |
| `SMTP_HOSTNAME` | The SMTP IP address or hostname | `127.0.0.1` | | `WG_CONF_TEMPLATE` | The custom `wg.conf` config file template. Please refer to our [default template](https://github.com/ngoduykhanh/wireguard-ui/blob/master/templates/wg.conf) | N/A |
| `SMTP_PORT` | The SMTP port | `25` | | `EMAIL_FROM_ADDRESS` | The sender email address | N/A |
| `SMTP_USERNAME` | The SMTP username | N/A | | `EMAIL_FROM_NAME` | The sender name | `WireGuard UI` |
| `SMTP_PASSWORD` | The SMTP user password | N/A | | `SENDGRID_API_KEY` | The SendGrid api key | N/A |
| `SMTP_AUTH_TYPE` | The SMTP authentication type. Possible values: `PLAIN`, `LOGIN`, `NONE` | `NONE` | | `SENDGRID_API_KEY_FILE` | Optional filepath for the SendGrid api key. Leave `SENDGRID_API_KEY` blank to take effect | N/A |
| `SMTP_ENCRYPTION` | the encryption method. Possible values: `NONE`, `SSL`, `SSLTLS`, `TLS`, `STARTTLS` | `STARTTLS` | | `SMTP_HOSTNAME` | The SMTP IP address or hostname | `127.0.0.1` |
| `SMTP_HELO` | Hostname to use for the HELO message. smtp-relay.gmail.com needs this set to anything but `localhost` | `localhost` | | `SMTP_PORT` | The SMTP port | `25` |
| `SMTP_USERNAME` | The SMTP username | N/A |
| `SMTP_PASSWORD` | The SMTP user password | N/A |
| `SMTP_PASSWORD_FILE` | Optional filepath for the SMTP user password. Leave `SMTP_PASSWORD` blank to take effect | N/A |
| `SMTP_AUTH_TYPE` | The SMTP authentication type. Possible values: `PLAIN`, `LOGIN`, `NONE` | `NONE` |
| `SMTP_ENCRYPTION` | The encryption method. Possible values: `NONE`, `SSL`, `SSLTLS`, `TLS`, `STARTTLS` | `STARTTLS` |
| `SMTP_HELO` | Hostname to use for the HELO message. smtp-relay.gmail.com needs this set to anything but `localhost` | `localhost` |
| `TELEGRAM_TOKEN` | Telegram bot token for distributing configs to clients | N/A |
| `TELEGRAM_ALLOW_CONF_REQUEST` | Allow users to get configs from the bot by sending a message | `false` |
| `TELEGRAM_FLOOD_WAIT` | Time in minutes before the next conf request is processed | `60` |
### Defaults for server configuration ### Defaults for server configuration

View file

@ -1,5 +1,20 @@
function renderClientList(data) { function renderClientList(data) {
$.each(data, function(index, obj) { $.each(data, function(index, obj) {
// render telegram button
let telegramButton = ''
if (obj.Client.telegram_userid) {
telegramButton = `<div class="btn-group">
<button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal"
data-target="#modal_telegram_client" data-clientid="${obj.Client.id}"
data-clientname="${obj.Client.name}">Telegram</button>
</div>`
}
let telegramHtml = "";
if (obj.Client.telegram_userid && obj.Client.telegram_userid.length > 0) {
telegramHtml = `<span class="info-box-text" style="display: none"><i class="fas fa-tguserid"></i>${obj.Client.telegram_userid}</span>`
}
// render client status css tag style // render client status css tag style
let clientStatusHtml = '>' let clientStatusHtml = '>'
if (obj.Client.enabled) { if (obj.Client.enabled) {
@ -18,6 +33,11 @@ function renderClientList(data) {
allowedIpsHtml += `<small class="badge badge-secondary">${obj}</small>&nbsp;`; allowedIpsHtml += `<small class="badge badge-secondary">${obj}</small>&nbsp;`;
}) })
let subnetRangesString = "";
if (obj.Client.subnet_ranges && obj.Client.subnet_ranges.length > 0) {
subnetRangesString = obj.Client.subnet_ranges.join(',')
}
// render client html content // render client html content
let html = `<div class="col-sm-6 col-md-6 col-lg-4" id="client_${obj.Client.id}"> let html = `<div class="col-sm-6 col-md-6 col-lg-4" id="client_${obj.Client.id}">
<div class="info-box"> <div class="info-box">
@ -38,7 +58,7 @@ function renderClientList(data) {
data-target="#modal_email_client" data-clientid="${obj.Client.id}" data-target="#modal_email_client" data-clientid="${obj.Client.id}"
data-clientname="${obj.Client.name}">Email</button> data-clientname="${obj.Client.name}">Email</button>
</div> </div>
${telegramButton}
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-outline-danger btn-sm">More</button> <button type="button" class="btn btn-outline-danger btn-sm">More</button>
<button type="button" class="btn btn-outline-danger btn-sm dropdown-toggle dropdown-icon" <button type="button" class="btn btn-outline-danger btn-sm dropdown-toggle dropdown-icon"
@ -59,6 +79,8 @@ function renderClientList(data) {
<hr> <hr>
<span class="info-box-text"><i class="fas fa-user"></i> ${obj.Client.name}</span> <span class="info-box-text"><i class="fas fa-user"></i> ${obj.Client.name}</span>
<span class="info-box-text" style="display: none"><i class="fas fa-key"></i> ${obj.Client.public_key}</span> <span class="info-box-text" style="display: none"><i class="fas fa-key"></i> ${obj.Client.public_key}</span>
<span class="info-box-text" style="display: none"><i class="fas fa-subnetrange"></i>${subnetRangesString}</span>
${telegramHtml}
<span class="info-box-text"><i class="fas fa-envelope"></i> ${obj.Client.email}</span> <span class="info-box-text"><i class="fas fa-envelope"></i> ${obj.Client.email}</span>
<span class="info-box-text"><i class="fas fa-clock"></i> <span class="info-box-text"><i class="fas fa-clock"></i>
${prettyDateTime(obj.Client.created_at)}</span> ${prettyDateTime(obj.Client.created_at)}</span>

View file

@ -13,7 +13,7 @@ type SmtpMail struct {
port int port int
username string username string
password string password string
SmtpHelo string smtpHelo string
authType mail.AuthType authType mail.AuthType
encryption mail.Encryption encryption mail.Encryption
noTLSCheck bool noTLSCheck bool
@ -48,7 +48,7 @@ func encryptionType(encryptionType string) mail.Encryption {
} }
func NewSmtpMail(hostname string, port int, username string, password string, SmtpHelo string, noTLSCheck bool, auth string, fromName, from string, encryption string) *SmtpMail { func NewSmtpMail(hostname string, port int, username string, password string, 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)} ans := SmtpMail{hostname: hostname, port: port, username: username, password: password, smtpHelo: SmtpHelo, noTLSCheck: noTLSCheck, fromName: fromName, from: from, authType: authType(auth), encryption: encryptionType(encryption)}
return &ans return &ans
} }

63
go.mod
View file

@ -1,27 +1,52 @@
module github.com/ngoduykhanh/wireguard-ui module github.com/ngoduykhanh/wireguard-ui
go 1.16 go 1.21
require ( require (
github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd github.com/NicoNex/echotron/v3 v3.27.0
github.com/go-playground/universal-translator v0.17.0 // indirect github.com/glendc/go-external-ip v0.1.0
github.com/gorilla/sessions v1.2.0 github.com/gorilla/sessions v1.2.2
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 // indirect github.com/labstack/echo-contrib v0.15.0
github.com/labstack/echo-contrib v0.9.0 github.com/labstack/echo/v4 v4.11.4
github.com/labstack/echo/v4 v4.1.16 github.com/labstack/gommon v0.4.2
github.com/labstack/gommon v0.3.0 github.com/rs/xid v1.5.0
github.com/leodido/go-urn v1.2.0 // indirect
github.com/rs/xid v1.2.1
github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d
github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8
github.com/sendgrid/rest v2.6.4+incompatible // indirect github.com/sendgrid/sendgrid-go v3.14.0+incompatible
github.com/sendgrid/sendgrid-go v3.10.0+incompatible github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 github.com/xhit/go-simple-mail/v2 v2.16.0
github.com/xhit/go-simple-mail/v2 v2.10.0 golang.org/x/crypto v0.17.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/mod v0.14.0
golang.org/x/mod v0.7.0
//golang.zx2c4.com/wireguard v0.0.20200121 // indirect //golang.zx2c4.com/wireguard v0.0.20200121 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v9 v9.31.0 gopkg.in/go-playground/validator.v9 v9.31.0
) )
require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-test/deep v1.1.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.0 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
)

314
go.sum
View file

@ -1,255 +1,119 @@
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/NicoNex/echotron/v3 v3.27.0 h1:iq4BLPO+Dz1JHjh2HPk0D0NldAZSYcAjaOicgYEhUzw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/NicoNex/echotron/v3 v3.27.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/casbin/casbin/v2 v2.0.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/bbolt v1.3.1-coreos.6.0.20180223184059-4f5275f4ebbf/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.1-coreos.6.0.20180223184059-4f5275f4ebbf/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/glendc/go-external-ip v0.1.0 h1:iX3xQ2Q26atAmLTbd++nUce2P5ht5P4uD4V7caSY/xg=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/glendc/go-external-ip v0.1.0/go.mod h1:CNx312s2FLAJoWNdJWZ2Fpf5O4oLsMFwuYviHjS4uJE=
github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd h1:1BzxHapafGJd/XlpMvocLeDBin2EKn90gXv2AQt5sfo= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd/go.mod h1:o9OoDQyE1WHvYVUH1FdFapy1/rCZHHq3O5wS4VA83ig= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8= github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8=
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw= github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw=
github.com/jessevdk/go-flags v0.0.0-20150816100521-1acbbaff2f34/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v0.0.0-20150816100521-1acbbaff2f34/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/labstack/echo-contrib v0.9.0 h1:hKBA2SnxdxR7sghH0J04zq/pImnKRmgvmQ6MvY9hug4=
github.com/labstack/echo-contrib v0.9.0/go.mod h1:TsFE5Vv0LRpZLoh4mMmaaAxzcTH+1CBFiUtVhwlegzU=
github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE=
github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o=
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc= 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.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
github.com/mdlayher/netlink v1.4.0 h1:n3ARR+Fm0dDv37dj5wSWZXDKcy+U0zwcXS3zKMnSiT0=
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/sabhiram/go-colorize v0.0.0-20210403184538-366f55d711cf/go.mod h1:GvlEbMJBpbAXFn06UajbdBlGZ18iLvHyuIrgG//L8uk= github.com/sabhiram/go-colorize v0.0.0-20210403184538-366f55d711cf/go.mod h1:GvlEbMJBpbAXFn06UajbdBlGZ18iLvHyuIrgG//L8uk=
github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d h1:NDtoSmsxTpDYTqvUurn2ooAzDaYbJSB9/tOhLzaewgo= github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d h1:NDtoSmsxTpDYTqvUurn2ooAzDaYbJSB9/tOhLzaewgo=
github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d/go.mod h1:SVPBBd492Gk7Cq5lPd6OAYtIGk2r1FsyH8KT3IB8h7c= github.com/sabhiram/go-wol v0.0.0-20211224004021-c83b0c2f887d/go.mod h1:SVPBBd492Gk7Cq5lPd6OAYtIGk2r1FsyH8KT3IB8h7c=
github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba h1:8QAc9wFAf2b/9cAXskm0wBylObZ0bTpRcaP7ThjLPVQ= github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8 h1:hlNRl87eAZhh2QMJVShuXHL6OOd0ObZM0JozDIruNeM=
github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba/go.mod h1:W6zxGUBCXRR5QugSd/nFcFVmwoGnvpjiNY/JwT03Wew= github.com/sdomino/scribble v0.0.0-20230717151034-b95d4df19aa8/go.mod h1:W6zxGUBCXRR5QugSd/nFcFVmwoGnvpjiNY/JwT03Wew=
github.com/sendgrid/rest v2.6.4+incompatible h1:lq6gAQxLwVBf3mVyCCSHI6mgF+NfaJFJHjT0kl6SSo8= github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.4+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.10.0+incompatible h1:aSYyurHxEZSDy7kxhvZ4fH0inNkEEmRssZNbAmETR2c= github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
github.com/sendgrid/sendgrid-go v3.10.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v0.0.0-20150929183540-2b15294402a8/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v0.0.0-20150929183540-2b15294402a8/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/uber/jaeger-client-go v2.19.1-0.20191002155754-0be28c34dabf+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
github.com/xhit/go-simple-mail/v2 v2.10.0 h1:nib6RaJ4qVh5HD9UE9QJqnUZyWp3upv+Z6CFxaMj0V8= github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/xhit/go-simple-mail/v2 v2.10.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-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-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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b h1:XDLXhn7ryprJVo+Lpkiib6CIuXE2031GDwtfEm7vLjI=
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M=
gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View file

@ -8,7 +8,9 @@ import (
"io/fs" "io/fs"
"net/http" "net/http"
"os" "os"
"regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
@ -17,15 +19,19 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
"github.com/rs/xid" "github.com/rs/xid"
"github.com/skip2/go-qrcode"
"golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/ngoduykhanh/wireguard-ui/emailer" "github.com/ngoduykhanh/wireguard-ui/emailer"
"github.com/ngoduykhanh/wireguard-ui/model" "github.com/ngoduykhanh/wireguard-ui/model"
"github.com/ngoduykhanh/wireguard-ui/store" "github.com/ngoduykhanh/wireguard-ui/store"
"github.com/ngoduykhanh/wireguard-ui/telegram"
"github.com/ngoduykhanh/wireguard-ui/util" "github.com/ngoduykhanh/wireguard-ui/util"
) )
var usernameRegexp = regexp.MustCompile("^\\w[\\w\\-.]*$")
// Health check handler // Health check handler
func Health() echo.HandlerFunc { func Health() echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -63,6 +69,10 @@ func Login(db store.IStore) echo.HandlerFunc {
password := data["password"].(string) password := data["password"].(string)
rememberMe := data["rememberMe"].(bool) rememberMe := data["rememberMe"].(bool)
if !usernameRegexp.MatchString(username) {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"})
}
dbuser, err := db.GetUserByName(username) dbuser, err := db.GetUserByName(username)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot query user from DB"}) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot query user from DB"})
@ -135,9 +145,12 @@ func GetUsers(db store.IStore) echo.HandlerFunc {
// GetUser handler returns a JSON object of single user // GetUser handler returns a JSON object of single user
func GetUser(db store.IStore) echo.HandlerFunc { func GetUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
username := c.Param("username") 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)) { if !isAdmin(c) && (username != currentUser(c)) {
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"}) return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"})
} }
@ -200,12 +213,16 @@ func UpdateUser(db store.IStore) echo.HandlerFunc {
admin = false admin = false
} }
if !usernameRegexp.MatchString(previousUsername) {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"})
}
user, err := db.GetUserByName(previousUsername) user, err := db.GetUserByName(previousUsername)
if err != nil { if err != nil {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()})
} }
if username == "" { if username == "" || !usernameRegexp.MatchString(username) {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"})
} else { } else {
user.Username = username user.Username = username
@ -261,7 +278,7 @@ func CreateUser(db store.IStore) echo.HandlerFunc {
password := data["password"].(string) password := data["password"].(string)
admin := data["admin"].(bool) admin := data["admin"].(bool)
if username == "" { if username == "" || !usernameRegexp.MatchString(username) {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"})
} else { } else {
user.Username = username user.Username = username
@ -303,6 +320,10 @@ func RemoveUser(db store.IStore) echo.HandlerFunc {
username := data["username"].(string) username := data["username"].(string)
if !usernameRegexp.MatchString(username) {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"})
}
if username == currentUser(c) { if username == currentUser(c) {
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "User cannot delete itself"}) return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "User cannot delete itself"})
} }
@ -348,6 +369,10 @@ func GetClients(db store.IStore) echo.HandlerFunc {
}) })
} }
for i, clientData := range clientDataList {
clientDataList[i] = util.FillClientSubnetRange(clientData)
}
return c.JSON(http.StatusOK, clientDataList) return c.JSON(http.StatusOK, clientDataList)
} }
} }
@ -357,10 +382,15 @@ func GetClient(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
clientID := c.Param("id") clientID := c.Param("id")
if _, err := xid.FromString(clientID); err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"})
}
qrCodeSettings := model.QRCodeSettings{ qrCodeSettings := model.QRCodeSettings{
Enabled: true, Enabled: true,
IncludeDNS: true, IncludeDNS: true,
IncludeMTU: true, IncludeMTU: true,
} }
clientData, err := db.GetClientByID(clientID, qrCodeSettings) clientData, err := db.GetClientByID(clientID, qrCodeSettings)
@ -368,7 +398,7 @@ func GetClient(db store.IStore) echo.HandlerFunc {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"})
} }
return c.JSON(http.StatusOK, clientData) return c.JSON(http.StatusOK, util.FillClientSubnetRange(clientData))
} }
} }
@ -379,6 +409,14 @@ func NewClient(db store.IStore) echo.HandlerFunc {
var client model.Client var client model.Client
c.Bind(&client) c.Bind(&client)
// Validate Telegram userid if provided
if client.TgUserid != "" {
idNum, err := strconv.ParseInt(client.TgUserid, 10, 64)
if err != nil || idNum == 0 {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Telegram userid must be a non-zero number"})
}
}
// read server information // read server information
server, err := db.GetServer() server, err := db.GetServer()
if err != nil { if err != nil {
@ -485,10 +523,14 @@ func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailCon
c.Bind(&payload) c.Bind(&payload)
// TODO validate email // TODO validate email
if _, err := xid.FromString(payload.ID); err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"})
}
qrCodeSettings := model.QRCodeSettings{ qrCodeSettings := model.QRCodeSettings{
Enabled: true, Enabled: true,
IncludeDNS: true, IncludeDNS: true,
IncludeMTU: true, IncludeMTU: true,
} }
clientData, err := db.GetClientByID(payload.ID, qrCodeSettings) clientData, err := db.GetClientByID(payload.ID, qrCodeSettings)
if err != nil { if err != nil {
@ -529,6 +571,51 @@ func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailCon
} }
} }
// SendTelegramClient handler to send the configuration via Telegram
func SendTelegramClient(db store.IStore) echo.HandlerFunc {
type clientIdUseridPayload struct {
ID string `json:"id"`
Userid string `json:"userid"`
}
return func(c echo.Context) error {
var payload clientIdUseridPayload
c.Bind(&payload)
clientData, err := db.GetClientByID(payload.ID, model.QRCodeSettings{Enabled: false})
if err != nil {
log.Errorf("Cannot generate client id %s config file for downloading: %v", payload.ID, err)
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"})
}
// build config
server, _ := db.GetServer()
globalSettings, _ := db.GetGlobalSettings()
config := util.BuildClientConfig(*clientData.Client, server, globalSettings)
configData := []byte(config)
var qrData []byte
if clientData.Client.PrivateKey != "" {
qrData, err = qrcode.Encode(config, qrcode.Medium, 512)
if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "qr gen: " + err.Error()})
}
}
userid, err := strconv.ParseInt(clientData.Client.TgUserid, 10, 64)
if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "userid: " + err.Error()})
}
err = telegram.SendConfig(userid, clientData.Client.Name, configData, qrData, false)
if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Telegram message sent successfully"})
}
}
// UpdateClient handler to update client information // UpdateClient handler to update client information
func UpdateClient(db store.IStore) echo.HandlerFunc { func UpdateClient(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -536,12 +623,24 @@ func UpdateClient(db store.IStore) echo.HandlerFunc {
var _client model.Client var _client model.Client
c.Bind(&_client) c.Bind(&_client)
if _, err := xid.FromString(_client.ID); err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"})
}
// validate client existence // validate client existence
clientData, err := db.GetClientByID(_client.ID, model.QRCodeSettings{Enabled: false}) clientData, err := db.GetClientByID(_client.ID, model.QRCodeSettings{Enabled: false})
if err != nil { if err != nil {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"})
} }
// Validate Telegram userid if provided
if _client.TgUserid != "" {
idNum, err := strconv.ParseInt(_client.TgUserid, 10, 64)
if err != nil || idNum == 0 {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Telegram userid must be a non-zero number"})
}
}
server, err := db.GetServer() server, err := db.GetServer()
if err != nil { if err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{ return c.JSON(http.StatusBadRequest, jsonHTTPResponse{
@ -609,11 +708,13 @@ func UpdateClient(db store.IStore) echo.HandlerFunc {
// map new data // map new data
client.Name = _client.Name client.Name = _client.Name
client.Email = _client.Email client.Email = _client.Email
client.TgUserid = _client.TgUserid
client.Enabled = _client.Enabled client.Enabled = _client.Enabled
client.UseServerDNS = _client.UseServerDNS client.UseServerDNS = _client.UseServerDNS
client.AllocatedIPs = _client.AllocatedIPs client.AllocatedIPs = _client.AllocatedIPs
client.AllowedIPs = _client.AllowedIPs client.AllowedIPs = _client.AllowedIPs
client.ExtraAllowedIPs = _client.ExtraAllowedIPs client.ExtraAllowedIPs = _client.ExtraAllowedIPs
client.Endpoint = _client.Endpoint
client.PublicKey = _client.PublicKey client.PublicKey = _client.PublicKey
client.PresharedKey = _client.PresharedKey client.PresharedKey = _client.PresharedKey
client.UpdatedAt = time.Now().UTC() client.UpdatedAt = time.Now().UTC()
@ -642,6 +743,10 @@ func SetClientStatus(db store.IStore) echo.HandlerFunc {
clientID := data["id"].(string) clientID := data["id"].(string)
status := data["status"].(bool) status := data["status"].(bool)
if _, err := xid.FromString(clientID); err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"})
}
clientData, err := db.GetClientByID(clientID, model.QRCodeSettings{Enabled: false}) clientData, err := db.GetClientByID(clientID, model.QRCodeSettings{Enabled: false})
if err != nil { if err != nil {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()})
@ -667,6 +772,10 @@ func DownloadClient(db store.IStore) echo.HandlerFunc {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Missing clientid parameter"}) return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Missing clientid parameter"})
} }
if _, err := xid.FromString(clientID); err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"})
}
clientData, err := db.GetClientByID(clientID, model.QRCodeSettings{Enabled: false}) clientData, err := db.GetClientByID(clientID, model.QRCodeSettings{Enabled: false})
if err != nil { if err != nil {
log.Errorf("Cannot generate client id %s config file for downloading: %v", clientID, err) log.Errorf("Cannot generate client id %s config file for downloading: %v", clientID, err)
@ -689,7 +798,7 @@ func DownloadClient(db store.IStore) echo.HandlerFunc {
// set response header for downloading // set response header for downloading
c.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%s.conf", clientData.Client.Name)) c.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%s.conf", clientData.Client.Name))
return c.Stream(http.StatusOK, "text/plain", reader) return c.Stream(http.StatusOK, "text/conf", reader)
} }
} }
@ -700,6 +809,10 @@ func RemoveClient(db store.IStore) echo.HandlerFunc {
client := new(model.Client) client := new(model.Client)
c.Bind(client) c.Bind(client)
if _, err := xid.FromString(client.ID); err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"})
}
// delete client from database // delete client from database
if err := db.DeleteClient(client.ID); err != nil { if err := db.DeleteClient(client.ID); err != nil {
@ -944,6 +1057,13 @@ func MachineIPAddresses() echo.HandlerFunc {
} }
} }
// GetOrderedSubnetRanges handler to get the ordered list of subnet ranges
func GetOrderedSubnetRanges() echo.HandlerFunc {
return func(c echo.Context) error {
return c.JSON(http.StatusOK, util.SubnetRangesOrder)
}
}
// SuggestIPAllocation handler to get the list of ip address for client // SuggestIPAllocation handler to get the list of ip address for client
func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { func SuggestIPAllocation(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -965,22 +1085,48 @@ func SuggestIPAllocation(db store.IStore) echo.HandlerFunc {
false, "Cannot suggest ip allocation: failed to get list of allocated ip addresses", false, "Cannot suggest ip allocation: failed to get list of allocated ip addresses",
}) })
} }
for _, cidr := range server.Interface.Addresses {
ip, err := util.GetAvailableIP(cidr, allocatedIPs) sr := c.QueryParam("sr")
searchCIDRList := make([]string, 0)
found := false
// Use subnet range or default to interface addresses
if util.SubnetRanges[sr] != nil {
for _, cidr := range util.SubnetRanges[sr] {
searchCIDRList = append(searchCIDRList, cidr.String())
}
} else {
searchCIDRList = append(searchCIDRList, server.Interface.Addresses...)
}
// Save only unique IPs
ipSet := make(map[string]struct{})
for _, cidr := range searchCIDRList {
ip, err := util.GetAvailableIP(cidr, allocatedIPs, server.Interface.Addresses)
if err != nil { if err != nil {
log.Error("Failed to get available ip from a CIDR: ", err) log.Error("Failed to get available ip from a CIDR: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ continue
false,
fmt.Sprintf("Cannot suggest ip allocation: failed to get available ip from network %s", cidr),
})
} }
found = true
if strings.Contains(ip, ":") { if strings.Contains(ip, ":") {
suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/128", ip)) ipSet[fmt.Sprintf("%s/128", ip)] = struct{}{}
} else { } else {
suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/32", ip)) ipSet[fmt.Sprintf("%s/32", ip)] = struct{}{}
} }
} }
if !found {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
false,
"Cannot suggest ip allocation: failed to get available ip. Try a different subnet or deallocate some ips.",
})
}
for ip := range ipSet {
suggestedIPs = append(suggestedIPs, ip)
}
return c.JSON(http.StatusOK, suggestedIPs) return c.JSON(http.StatusOK, suggestedIPs)
} }
} }
@ -1034,7 +1180,6 @@ func ApplyServerConfig(db store.IStore, tmplDir fs.FS) echo.HandlerFunc {
} }
} }
// GetHashesChanges handler returns if database hashes have changed // GetHashesChanges handler returns if database hashes have changed
func GetHashesChanges(db store.IStore) echo.HandlerFunc { func GetHashesChanges(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {

161
main.go
View file

@ -4,13 +4,18 @@ import (
"embed" "embed"
"flag" "flag"
"fmt" "fmt"
"io/fs"
"net"
"net/http"
"os"
"strings"
"syscall"
"time"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
"github.com/ngoduykhanh/wireguard-ui/store" "github.com/ngoduykhanh/wireguard-ui/store"
"io/fs" "github.com/ngoduykhanh/wireguard-ui/telegram"
"net/http"
"os"
"time"
"github.com/ngoduykhanh/wireguard-ui/emailer" "github.com/ngoduykhanh/wireguard-ui/emailer"
"github.com/ngoduykhanh/wireguard-ui/handler" "github.com/ngoduykhanh/wireguard-ui/handler"
@ -26,22 +31,26 @@ var (
gitRef = "N/A" gitRef = "N/A"
buildTime = fmt.Sprintf(time.Now().UTC().Format("01-02-2006 15:04:05")) buildTime = fmt.Sprintf(time.Now().UTC().Format("01-02-2006 15:04:05"))
// configuration variables // configuration variables
flagDisableLogin bool = false flagDisableLogin bool = false
flagBindAddress string = "0.0.0.0:5000" flagBindAddress string = "0.0.0.0:5000"
flagSmtpHostname string = "127.0.0.1" flagSmtpHostname string = "127.0.0.1"
flagSmtpPort int = 25 flagSmtpPort int = 25
flagSmtpHelo string = "localhost" flagSmtpUsername string
flagSmtpUsername string flagSmtpPassword string
flagSmtpPassword string flagSmtpAuthType string = "NONE"
flagSmtpAuthType string = "NONE" flagSmtpNoTLSCheck bool = false
flagSmtpNoTLSCheck bool = false flagSmtpEncryption string = "STARTTLS"
flagSmtpEncryption string = "STARTTLS" flagSmtpHelo string = "localhost"
flagSendgridApiKey string flagSendgridApiKey string
flagEmailFrom string flagEmailFrom string
flagEmailFromName string = "WireGuard UI" flagEmailFromName string = "WireGuard UI"
flagSessionSecret string = util.RandomString(32) flagTelegramToken string
flagWgConfTemplate string flagTelegramAllowConfRequest bool = false
flagBasePath string flagTelegramFloodWait int = 60
flagSessionSecret string = util.RandomString(32)
flagWgConfTemplate string
flagBasePath string
flagSubnetRanges string
) )
const ( const (
@ -72,16 +81,45 @@ func init() {
flag.IntVar(&flagSmtpPort, "smtp-port", util.LookupEnvOrInt("SMTP_PORT", flagSmtpPort), "SMTP Port") flag.IntVar(&flagSmtpPort, "smtp-port", util.LookupEnvOrInt("SMTP_PORT", flagSmtpPort), "SMTP Port")
flag.StringVar(&flagSmtpHelo, "smtp-helo", util.LookupEnvOrString("SMTP_HELO", flagSmtpHelo), "SMTP HELO Hostname") flag.StringVar(&flagSmtpHelo, "smtp-helo", util.LookupEnvOrString("SMTP_HELO", flagSmtpHelo), "SMTP HELO Hostname")
flag.StringVar(&flagSmtpUsername, "smtp-username", util.LookupEnvOrString("SMTP_USERNAME", flagSmtpUsername), "SMTP Username") flag.StringVar(&flagSmtpUsername, "smtp-username", util.LookupEnvOrString("SMTP_USERNAME", flagSmtpUsername), "SMTP Username")
flag.StringVar(&flagSmtpPassword, "smtp-password", util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword), "SMTP Password")
flag.BoolVar(&flagSmtpNoTLSCheck, "smtp-no-tls-check", util.LookupEnvOrBool("SMTP_NO_TLS_CHECK", flagSmtpNoTLSCheck), "Disable TLS verification for SMTP. This is potentially dangerous.") flag.BoolVar(&flagSmtpNoTLSCheck, "smtp-no-tls-check", util.LookupEnvOrBool("SMTP_NO_TLS_CHECK", flagSmtpNoTLSCheck), "Disable TLS verification for SMTP. This is potentially dangerous.")
flag.StringVar(&flagSmtpEncryption, "smtp-encryption", util.LookupEnvOrString("SMTP_ENCRYPTION", flagSmtpEncryption), "SMTP Encryption : NONE, SSL, SSLTLS, TLS or STARTTLS (by default)") flag.StringVar(&flagSmtpEncryption, "smtp-encryption", util.LookupEnvOrString("SMTP_ENCRYPTION", flagSmtpEncryption), "SMTP Encryption : NONE, SSL, SSLTLS, TLS or STARTTLS (by default)")
flag.StringVar(&flagSmtpAuthType, "smtp-auth-type", util.LookupEnvOrString("SMTP_AUTH_TYPE", flagSmtpAuthType), "SMTP Auth Type : PLAIN, LOGIN or NONE.") flag.StringVar(&flagSmtpAuthType, "smtp-auth-type", util.LookupEnvOrString("SMTP_AUTH_TYPE", flagSmtpAuthType), "SMTP Auth Type : PLAIN, LOGIN or NONE.")
flag.StringVar(&flagSendgridApiKey, "sendgrid-api-key", util.LookupEnvOrString("SENDGRID_API_KEY", flagSendgridApiKey), "Your sendgrid api key.")
flag.StringVar(&flagEmailFrom, "email-from", util.LookupEnvOrString("EMAIL_FROM_ADDRESS", flagEmailFrom), "'From' email address.") flag.StringVar(&flagEmailFrom, "email-from", util.LookupEnvOrString("EMAIL_FROM_ADDRESS", flagEmailFrom), "'From' email address.")
flag.StringVar(&flagEmailFromName, "email-from-name", util.LookupEnvOrString("EMAIL_FROM_NAME", flagEmailFromName), "'From' email name.") flag.StringVar(&flagEmailFromName, "email-from-name", util.LookupEnvOrString("EMAIL_FROM_NAME", flagEmailFromName), "'From' email name.")
flag.StringVar(&flagSessionSecret, "session-secret", util.LookupEnvOrString("SESSION_SECRET", flagSessionSecret), "The key used to encrypt session cookies.") flag.StringVar(&flagTelegramToken, "telegram-token", util.LookupEnvOrString("TELEGRAM_TOKEN", flagTelegramToken), "Telegram bot token for distributing configs to clients.")
flag.BoolVar(&flagTelegramAllowConfRequest, "telegram-allow-conf-request", util.LookupEnvOrBool("TELEGRAM_ALLOW_CONF_REQUEST", flagTelegramAllowConfRequest), "Allow users to get configs from the bot by sending a message.")
flag.IntVar(&flagTelegramFloodWait, "telegram-flood-wait", util.LookupEnvOrInt("TELEGRAM_FLOOD_WAIT", flagTelegramFloodWait), "Time in minutes before the next conf request is processed.")
flag.StringVar(&flagWgConfTemplate, "wg-conf-template", util.LookupEnvOrString("WG_CONF_TEMPLATE", flagWgConfTemplate), "Path to custom wg.conf template.") flag.StringVar(&flagWgConfTemplate, "wg-conf-template", util.LookupEnvOrString("WG_CONF_TEMPLATE", flagWgConfTemplate), "Path to custom wg.conf template.")
flag.StringVar(&flagBasePath, "base-path", util.LookupEnvOrString("BASE_PATH", flagBasePath), "The base path of the URL") flag.StringVar(&flagBasePath, "base-path", util.LookupEnvOrString("BASE_PATH", flagBasePath), "The base path of the URL")
flag.StringVar(&flagSubnetRanges, "subnet-ranges", util.LookupEnvOrString("SUBNET_RANGES", flagSubnetRanges), "IP ranges to choose from when assigning an IP for a client.")
var (
smtpPasswordLookup = util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword)
sengridApiKeyLookup = 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 sengridApiKey env var
if sengridApiKeyLookup != "" {
flag.StringVar(&flagSendgridApiKey, "sendgrid-api-key", sengridApiKeyLookup, "Your sendgrid api key.")
} else {
flag.StringVar(&flagSendgridApiKey, "sendgrid-api-key", util.LookupEnvOrFile("SENDGRID_API_KEY_FILE", flagSendgridApiKey), "File containing your sendgrid api key.")
}
// check empty sessionSecret env var
if sessionSecretLookup != "" {
flag.StringVar(&flagSessionSecret, "session-secret", sessionSecretLookup, "The key used to encrypt session cookies.")
} else {
flag.StringVar(&flagSessionSecret, "session-secret", util.LookupEnvOrFile("SESSION_SECRET_FILE", flagSessionSecret), "File containing the key used to encrypt session cookies.")
}
flag.Parse() flag.Parse()
// update runtime config // update runtime config
@ -101,9 +139,17 @@ func init() {
util.SessionSecret = []byte(flagSessionSecret) util.SessionSecret = []byte(flagSessionSecret)
util.WgConfTemplate = flagWgConfTemplate util.WgConfTemplate = flagWgConfTemplate
util.BasePath = util.ParseBasePath(flagBasePath) util.BasePath = util.ParseBasePath(flagBasePath)
util.SubnetRanges = util.ParseSubnetRanges(flagSubnetRanges)
lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO"))
telegram.Token = flagTelegramToken
telegram.AllowConfRequest = flagTelegramAllowConfRequest
telegram.FloodWait = flagTelegramFloodWait
telegram.LogLevel = lvl
// print only if log level is INFO or lower // print only if log level is INFO or lower
if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO { if lvl <= log.INFO {
// print app information // print app information
fmt.Println("Wireguard UI") fmt.Println("Wireguard UI")
fmt.Println("App Version\t:", appVersion) fmt.Println("App Version\t:", appVersion)
@ -119,6 +165,7 @@ func init() {
//fmt.Println("Session secret\t:", util.SessionSecret) //fmt.Println("Session secret\t:", util.SessionSecret)
fmt.Println("Custom wg.conf\t:", util.WgConfTemplate) fmt.Println("Custom wg.conf\t:", util.WgConfTemplate)
fmt.Println("Base path\t:", util.BasePath+"/") fmt.Println("Base path\t:", util.BasePath+"/")
fmt.Println("Subnet ranges\t:", util.GetSubnetRangesString())
} }
} }
@ -137,27 +184,42 @@ func main() {
extraData["basePath"] = util.BasePath extraData["basePath"] = util.BasePath
extraData["loginDisabled"] = flagDisableLogin extraData["loginDisabled"] = flagDisableLogin
// strip the "templates/" prefix from the embedded directory so files can be read by their directname (e.g. // 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") // "base.html" instead of "templates/base.html")
tmplDir, _ := fs.Sub(fs.FS(embeddedTemplates), "templates") tmplDir, _ := fs.Sub(fs.FS(embeddedTemplates), "templates")
// create the wireguard config on start, if it doesn't exist // create the wireguard config on start, if it doesn't exist
initServerConfig(db, tmplDir) initServerConfig(db, tmplDir)
// Check if subnet ranges are valid for the server configuration
// Remove any non-valid CIDRs
if err := util.ValidateAndFixSubnetRanges(db); err != nil {
panic(err)
}
// Print valid ranges
if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO {
fmt.Println("Valid subnet ranges:", util.GetSubnetRangesString())
}
// register routes // register routes
app := router.New(tmplDir, extraData, util.SessionSecret) app := router.New(tmplDir, extraData, util.SessionSecret)
app.GET(util.BasePath, handler.WireGuardClients(db), handler.ValidSession) app.GET(util.BasePath, handler.WireGuardClients(db), handler.ValidSession)
// Important: Make sure that all non-GET routes check the request content type using handler.ContentTypeJson to
// mitigate CSRF attacks. This is effective, because browsers don't allow setting the Content-Type header on
// cross-origin requests.
if !util.DisableLogin { if !util.DisableLogin {
app.GET(util.BasePath+"/login", handler.LoginPage()) app.GET(util.BasePath+"/login", handler.LoginPage())
app.POST(util.BasePath+"/login", handler.Login(db)) app.POST(util.BasePath+"/login", handler.Login(db), handler.ContentTypeJson)
app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession) app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession)
app.GET(util.BasePath+"/profile", handler.LoadProfile(db), handler.ValidSession) app.GET(util.BasePath+"/profile", handler.LoadProfile(db), handler.ValidSession)
app.GET(util.BasePath+"/users-settings", handler.UsersSettings(db), handler.ValidSession,handler.NeedsAdmin) app.GET(util.BasePath+"/users-settings", handler.UsersSettings(db), handler.ValidSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/update-user", handler.UpdateUser(db), handler.ValidSession) 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.NeedsAdmin) 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.NeedsAdmin) app.POST(util.BasePath+"/remove-user", handler.RemoveUser(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.GET(util.BasePath+"/getusers", handler.GetUsers(db), handler.ValidSession, handler.NeedsAdmin) app.GET(util.BasePath+"/getusers", handler.GetUsers(db), handler.ValidSession, handler.NeedsAdmin)
app.GET(util.BasePath+"/api/user/:username", handler.GetUser(db), handler.ValidSession) app.GET(util.BasePath+"/api/user/:username", handler.GetUser(db), handler.ValidSession)
} }
@ -176,6 +238,7 @@ func main() {
app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/update-client", handler.UpdateClient(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/update-client", handler.UpdateClient(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/send-telegram-client", handler.SendTelegramClient(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson)
app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession) app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession)
@ -183,11 +246,12 @@ func main() {
app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.NeedsAdmin) app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession,handler.ContentTypeJson, handler.NeedsAdmin) app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession) app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession)
app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession) app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession)
app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession) app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession)
app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession) app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession)
app.GET(util.BasePath+"/api/subnet-ranges", handler.GetOrderedSubnetRanges(), handler.ValidSession)
app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession) app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession)
app.POST(util.BasePath+"/api/apply-wg-config", handler.ApplyServerConfig(db, tmplDir), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/api/apply-wg-config", handler.ApplyServerConfig(db, tmplDir), handler.ValidSession, handler.ContentTypeJson)
app.GET(util.BasePath+"/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession) app.GET(util.BasePath+"/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession)
@ -195,14 +259,34 @@ func main() {
app.DELETE(util.BasePath+"/wake_on_lan_host/:mac_address", handler.DeleteWakeOnHost(db), handler.ValidSession, handler.ContentTypeJson) app.DELETE(util.BasePath+"/wake_on_lan_host/:mac_address", handler.DeleteWakeOnHost(db), handler.ValidSession, handler.ContentTypeJson)
app.PUT(util.BasePath+"/wake_on_lan_host/:mac_address", handler.WakeOnHost(db), handler.ValidSession, handler.ContentTypeJson) app.PUT(util.BasePath+"/wake_on_lan_host/:mac_address", handler.WakeOnHost(db), handler.ValidSession, handler.ContentTypeJson)
// strip the "assets/" prefix from the embedded directory so files can be called directly withoutthe "assets/" // strip the "assets/" prefix from the embedded directory so files can be called directly without the "assets/"
// prefix // prefix
assetsDir, _ := fs.Sub(fs.FS(embeddedAssets), "assets") assetsDir, _ := fs.Sub(fs.FS(embeddedAssets), "assets")
assetHandler := http.FileServer(http.FS(assetsDir)) assetHandler := http.FileServer(http.FS(assetsDir))
// serves other static files // serves other static files
app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler))) app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler)))
app.Logger.Fatal(app.Start(util.BindAddress)) initDeps := telegram.TgBotInitDependencies{
DB: db,
SendRequestedConfigsToTelegram: util.SendRequestedConfigsToTelegram,
}
initTelegram(initDeps)
if strings.HasPrefix(util.BindAddress, "unix://") {
// Listen on unix domain socket.
// https://github.com/labstack/echo/issues/830
syscall.Unlink(util.BindAddress[6:])
l, err := net.Listen("unix", util.BindAddress[6:])
if err != nil {
app.Logger.Fatal(err)
}
app.Listener = l
app.Logger.Fatal(app.Start(""))
} else {
// Listen on TCP socket
app.Logger.Fatal(app.Start(util.BindAddress))
}
} }
func initServerConfig(db store.IStore, tmplDir fs.FS) { func initServerConfig(db store.IStore, tmplDir fs.FS) {
@ -237,3 +321,14 @@ func initServerConfig(db store.IStore, tmplDir fs.FS) {
log.Fatalf("Cannot create server config: ", err) log.Fatalf("Cannot create server config: ", err)
} }
} }
func initTelegram(initDeps telegram.TgBotInitDependencies) {
go func() {
for {
err := telegram.Start(initDeps)
if err == nil {
break
}
}
}()
}

View file

@ -11,10 +11,13 @@ type Client struct {
PublicKey string `json:"public_key"` PublicKey string `json:"public_key"`
PresharedKey string `json:"preshared_key"` PresharedKey string `json:"preshared_key"`
Name string `json:"name"` Name string `json:"name"`
TgUserid string `json:"telegram_userid"`
Email string `json:"email"` Email string `json:"email"`
SubnetRanges []string `json:"subnet_ranges,omitempty"`
AllocatedIPs []string `json:"allocated_ips"` AllocatedIPs []string `json:"allocated_ips"`
AllowedIPs []string `json:"allowed_ips"` AllowedIPs []string `json:"allowed_ips"`
ExtraAllowedIPs []string `json:"extra_allowed_ips"` ExtraAllowedIPs []string `json:"extra_allowed_ips"`
Endpoint string `json:"endpoint"`
UseServerDNS bool `json:"use_server_dns"` UseServerDNS bool `json:"use_server_dns"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
@ -28,7 +31,7 @@ type ClientData struct {
} }
type QRCodeSettings struct { type QRCodeSettings struct {
Enabled bool Enabled bool
IncludeDNS bool IncludeDNS bool
IncludeMTU bool IncludeMTU bool
} }

View file

@ -23,5 +23,6 @@ type ServerInterface struct {
ListenPort int `json:"listen_port,string"` // ,string to get listen_port string input as int ListenPort int `json:"listen_port,string"` // ,string to get listen_port string input as int
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
PostUp string `json:"post_up"` PostUp string `json:"post_up"`
PreDown string `json:"pre_down"`
PostDown string `json:"post_down"` PostDown string `json:"post_down"`
} }

View file

@ -2,6 +2,7 @@ package model
import ( import (
"errors" "errors"
"net"
"strings" "strings"
"time" "time"
) )
@ -18,7 +19,13 @@ func (host WakeOnLanHost) ResolveResourceName() (string, error) {
return "", errors.New("mac Address is Empty") return "", errors.New("mac Address is Empty")
} }
resourceName = strings.ToUpper(resourceName) resourceName = strings.ToUpper(resourceName)
return strings.ReplaceAll(resourceName, ":", "-"), nil resourceName = strings.ReplaceAll(resourceName, ":", "-")
if _, err := net.ParseMAC(resourceName); err != nil {
return "", errors.New("invalid mac address")
}
return resourceName, nil
} }
const WakeOnLanHostCollectionName = "wake_on_lan_hosts" const WakeOnLanHostCollectionName = "wake_on_lan_hosts"

View file

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"strconv"
"time" "time"
"github.com/sdomino/scribble" "github.com/sdomino/scribble"
@ -38,12 +39,12 @@ func New(dbPath string) (*JsonDB, error) {
func (o *JsonDB) Init() error { func (o *JsonDB) Init() error {
var clientPath string = path.Join(o.dbPath, "clients") var clientPath string = path.Join(o.dbPath, "clients")
var serverPath string = path.Join(o.dbPath, "server") var serverPath string = path.Join(o.dbPath, "server")
var userPath string = path.Join(o.dbPath, "users")
var wakeOnLanHostsPath string = path.Join(o.dbPath, "wake_on_lan_hosts") var wakeOnLanHostsPath string = path.Join(o.dbPath, "wake_on_lan_hosts")
var serverInterfacePath string = path.Join(serverPath, "interfaces.json") var serverInterfacePath string = path.Join(serverPath, "interfaces.json")
var serverKeyPairPath string = path.Join(serverPath, "keypair.json") var serverKeyPairPath string = path.Join(serverPath, "keypair.json")
var globalSettingPath string = path.Join(serverPath, "global_settings.json") var globalSettingPath string = path.Join(serverPath, "global_settings.json")
var hashesPath string = path.Join(serverPath, "hashes.json") var hashesPath string = path.Join(serverPath, "hashes.json")
var userPath string = path.Join(serverPath, "users.json")
// create directories if they do not exist // create directories if they do not exist
if _, err := os.Stat(clientPath); os.IsNotExist(err) { if _, err := os.Stat(clientPath); os.IsNotExist(err) {
@ -52,12 +53,12 @@ func (o *JsonDB) Init() error {
if _, err := os.Stat(serverPath); os.IsNotExist(err) { if _, err := os.Stat(serverPath); os.IsNotExist(err) {
os.MkdirAll(serverPath, os.ModePerm) os.MkdirAll(serverPath, os.ModePerm)
} }
if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) {
os.MkdirAll(wakeOnLanHostsPath, os.ModePerm)
}
if _, err := os.Stat(userPath); os.IsNotExist(err) { if _, err := os.Stat(userPath); os.IsNotExist(err) {
os.MkdirAll(userPath, os.ModePerm) os.MkdirAll(userPath, os.ModePerm)
} }
if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) {
os.MkdirAll(wakeOnLanHostsPath, os.ModePerm)
}
// server's interface // server's interface
if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) { if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) {
@ -68,7 +69,10 @@ func (o *JsonDB) Init() error {
serverInterface.PostDown = util.LookupEnvOrString(util.ServerPostDownScriptEnvVar, "") serverInterface.PostDown = util.LookupEnvOrString(util.ServerPostDownScriptEnvVar, "")
serverInterface.UpdatedAt = time.Now().UTC() serverInterface.UpdatedAt = time.Now().UTC()
o.conn.Write("server", "interfaces", serverInterface) o.conn.Write("server", "interfaces", serverInterface)
os.Chmod(serverInterfacePath, 0600) err := util.ManagePerms(serverInterfacePath)
if err != nil {
return err
}
} }
// server's key pair // server's key pair
@ -83,7 +87,10 @@ func (o *JsonDB) Init() error {
serverKeyPair.PublicKey = key.PublicKey().String() serverKeyPair.PublicKey = key.PublicKey().String()
serverKeyPair.UpdatedAt = time.Now().UTC() serverKeyPair.UpdatedAt = time.Now().UTC()
o.conn.Write("server", "keypair", serverKeyPair) o.conn.Write("server", "keypair", serverKeyPair)
os.Chmod(serverKeyPairPath, 0600) err = util.ManagePerms(serverKeyPairPath)
if err != nil {
return err
}
} }
// global settings // global settings
@ -108,7 +115,10 @@ func (o *JsonDB) Init() error {
globalSetting.ConfigFilePath = util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath) globalSetting.ConfigFilePath = util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath)
globalSetting.UpdatedAt = time.Now().UTC() globalSetting.UpdatedAt = time.Now().UTC()
o.conn.Write("server", "global_settings", globalSetting) o.conn.Write("server", "global_settings", globalSetting)
os.Chmod(globalSettingPath, 0600) err := util.ManagePerms(globalSettingPath)
if err != nil {
return err
}
} }
// hashes // hashes
@ -117,7 +127,10 @@ func (o *JsonDB) Init() error {
clientServerHashes.Client = "none" clientServerHashes.Client = "none"
clientServerHashes.Server = "none" clientServerHashes.Server = "none"
o.conn.Write("server", "hashes", clientServerHashes) o.conn.Write("server", "hashes", clientServerHashes)
os.Chmod(hashesPath, 0600) err := util.ManagePerms(hashesPath)
if err != nil {
return err
}
} }
// user info // user info
@ -128,26 +141,44 @@ func (o *JsonDB) Init() error {
user.Admin = util.DefaultIsAdmin user.Admin = util.DefaultIsAdmin
user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "") user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "")
if user.PasswordHash == "" { if user.PasswordHash == "" {
plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword) user.PasswordHash = util.LookupEnvOrFile(util.PasswordHashFileEnvVar, "")
hash, err := util.HashPassword(plaintext) if user.PasswordHash == "" {
if err != nil { plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword)
return err if plaintext == util.DefaultPassword {
plaintext = util.LookupEnvOrFile(util.PasswordFileEnvVar, util.DefaultPassword)
}
hash, err := util.HashPassword(plaintext)
if err != nil {
return err
}
user.PasswordHash = hash
} }
user.PasswordHash = hash
} }
o.conn.Write("users", user.Username, user) o.conn.Write("users", user.Username, user)
os.Chmod(path.Join(path.Join(o.dbPath, "users"), user.Username+".json"), 0600) err = util.ManagePerms(path.Join(path.Join(o.dbPath, "users"), user.Username+".json"))
if err != nil {
return err
}
}
// init cache
clients, err := o.GetClients(false)
if err != nil {
return nil
}
for _, cl := range clients {
client := cl.Client
if client.Enabled && len(client.TgUserid) > 0 {
if userid, err := strconv.ParseInt(client.TgUserid, 10, 64); err == nil {
util.UpdateTgToClientID(userid, client.ID)
}
}
} }
return nil return nil
} }
// GetUser func to query user info from the database
func (o *JsonDB) GetUser() (model.User, error) {
user := model.User{}
return user, o.conn.Read("server", "users", &user)
}
// GetUsers func to get all users from the database // GetUsers func to get all users from the database
func (o *JsonDB) GetUsers() ([]model.User, error) { func (o *JsonDB) GetUsers() ([]model.User, error) {
var users []model.User var users []model.User
@ -182,7 +213,10 @@ func (o *JsonDB) GetUserByName(username string) (model.User, error) {
func (o *JsonDB) SaveUser(user model.User) error { func (o *JsonDB) SaveUser(user model.User) error {
userPath := path.Join(path.Join(o.dbPath, "users"), user.Username+".json") userPath := path.Join(path.Join(o.dbPath, "users"), user.Username+".json")
output := o.conn.Write("users", user.Username, user) output := o.conn.Write("users", user.Username, user)
os.Chmod(userPath, 0600) err := util.ManagePerms(userPath)
if err != nil {
return err
}
return output return output
} }
@ -295,32 +329,56 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti
func (o *JsonDB) SaveClient(client model.Client) error { func (o *JsonDB) SaveClient(client model.Client) error {
clientPath := path.Join(path.Join(o.dbPath, "clients"), client.ID+".json") clientPath := path.Join(path.Join(o.dbPath, "clients"), client.ID+".json")
output := o.conn.Write("clients", client.ID, client) output := o.conn.Write("clients", client.ID, client)
os.Chmod(clientPath, 0600) 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 return output
} }
func (o *JsonDB) DeleteClient(clientID string) error { func (o *JsonDB) DeleteClient(clientID string) error {
util.RemoveTgToClientID(clientID)
return o.conn.Delete("clients", clientID) return o.conn.Delete("clients", clientID)
} }
func (o *JsonDB) SaveServerInterface(serverInterface model.ServerInterface) error { func (o *JsonDB) SaveServerInterface(serverInterface model.ServerInterface) error {
serverInterfacePath := path.Join(path.Join(o.dbPath, "server"), "interfaces.json") serverInterfacePath := path.Join(path.Join(o.dbPath, "server"), "interfaces.json")
output := o.conn.Write("server", "interfaces", serverInterface) output := o.conn.Write("server", "interfaces", serverInterface)
os.Chmod(serverInterfacePath, 0600) err := util.ManagePerms(serverInterfacePath)
if err != nil {
return err
}
return output return output
} }
func (o *JsonDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error { func (o *JsonDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error {
serverKeyPairPath := path.Join(path.Join(o.dbPath, "server"), "keypair.json") serverKeyPairPath := path.Join(path.Join(o.dbPath, "server"), "keypair.json")
output := o.conn.Write("server", "keypair", serverKeyPair) output := o.conn.Write("server", "keypair", serverKeyPair)
os.Chmod(serverKeyPairPath, 0600) err := util.ManagePerms(serverKeyPairPath)
if err != nil {
return err
}
return output return output
} }
func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error { func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error {
globalSettingsPath := path.Join(path.Join(o.dbPath, "server"), "global_settings.json") globalSettingsPath := path.Join(path.Join(o.dbPath, "server"), "global_settings.json")
output := o.conn.Write("server", "global_settings", globalSettings) output := o.conn.Write("server", "global_settings", globalSettings)
os.Chmod(globalSettingsPath, 0600) err := util.ManagePerms(globalSettingsPath)
if err != nil {
return err
}
return output return output
} }
@ -336,6 +394,9 @@ func (o *JsonDB) GetHashes() (model.ClientServerHashes, error) {
func (o *JsonDB) SaveHashes(hashes model.ClientServerHashes) error { func (o *JsonDB) SaveHashes(hashes model.ClientServerHashes) error {
hashesPath := path.Join(path.Join(o.dbPath, "server"), "hashes.json") hashesPath := path.Join(path.Join(o.dbPath, "server"), "hashes.json")
output := o.conn.Write("server", "hashes", hashes) output := o.conn.Write("server", "hashes", hashes)
os.Chmod(hashesPath, 0600) err := util.ManagePerms(hashesPath)
if err != nil {
return err
}
return output return output
} }

View file

@ -3,10 +3,10 @@ package jsondb
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"path" "path"
"github.com/ngoduykhanh/wireguard-ui/model" "github.com/ngoduykhanh/wireguard-ui/model"
"github.com/ngoduykhanh/wireguard-ui/util"
) )
func (o *JsonDB) GetWakeOnLanHosts() ([]model.WakeOnLanHost, error) { func (o *JsonDB) GetWakeOnLanHosts() ([]model.WakeOnLanHost, error) {
@ -70,7 +70,11 @@ func (o *JsonDB) SaveWakeOnLanHost(host model.WakeOnLanHost) error {
wakeOnLanHostPath := path.Join(path.Join(o.dbPath, model.WakeOnLanHostCollectionName), resourceName+".json") wakeOnLanHostPath := path.Join(path.Join(o.dbPath, model.WakeOnLanHostCollectionName), resourceName+".json")
output := o.conn.Write(model.WakeOnLanHostCollectionName, resourceName, host) output := o.conn.Write(model.WakeOnLanHostCollectionName, resourceName, host)
os.Chmod(wakeOnLanHostPath, 0600) err = util.ManagePerms(wakeOnLanHostPath)
if err != nil {
return err
}
return output return output
} }

155
telegram/bot.go Normal file
View file

@ -0,0 +1,155 @@
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, 0)
floodMessageSent = make(map[int64]struct{}, 0)
)
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{}{}
bot.SendMessage(
fmt.Sprintf("You can only request your configs once per %d minutes", FloodWait),
userid,
&echotron.MessageOptions{
ReplyToMessageID: update.Message.ID,
})
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"
}
bot.SendMessage(
messageText,
userid,
&echotron.MessageOptions{
ReplyToMessageID: update.Message.ID,
})
}
}
}
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)
}
}
}

View file

@ -58,11 +58,13 @@
</div> </div>
<div class="form-group form-group-sm"> <div class="form-group form-group-sm">
<select name="status-selector" id="status-selector" class="custom-select form-control-navbar" style="margin-left: 0.5em; height: 90%; font-size: 14px;"> <select name="status-selector" id="status-selector" class="custom-select form-control-navbar" style="margin-left: 0.5em; height: 90%; font-size: 14px;">
<!-- THIS SECTION IS OVERRIDDEN BY JS. SEE updateSearchList() function in clients.html BEFORE EDITING -->
<option value="All">All</option> <option value="All">All</option>
<option value="Enabled">Enabled</option> <option value="Enabled">Enabled</option>
<option value="Disabled">Disabled</option> <option value="Disabled">Disabled</option>
<option value="Connected">Connected</option> <option value="Connected">Connected</option>
<option value="Disconnected">Disconnected</option> <option value="Disconnected">Disconnected</option>
<!-- THIS SECTION IS OVERRIDDEN BY JS. SEE updateSearchList() function in clients.html BEFORE EDITING -->
</select> </select>
</div> </div>
</form> </form>
@ -209,6 +211,12 @@
<label for="client_email" class="control-label">Email</label> <label for="client_email" class="control-label">Email</label>
<input type="text" class="form-control" id="client_email" name="client_email"> <input type="text" class="form-control" id="client_email" name="client_email">
</div> </div>
<div class="form-group">
<label for="subnet_ranges" class="control-label">Subnet range</label>
<select id="subnet_ranges" class="select2"
data-placeholder="Select a subnet range" style="width: 100%;">
</select>
</div>
<div class="form-group"> <div class="form-group">
<label for="client_allocated_ips" class="control-label">IP Allocation</label> <label for="client_allocated_ips" class="control-label">IP Allocation</label>
<input type="text" data-role="tagsinput" class="form-control" id="client_allocated_ips"> <input type="text" data-role="tagsinput" class="form-control" id="client_allocated_ips">
@ -232,6 +240,10 @@
</label> </label>
<input type="text" data-role="tagsinput" class="form-control" id="client_extra_allowed_ips" value="{{ StringsJoin .client_defaults.ExtraAllowedIps "," }}"> <input type="text" data-role="tagsinput" class="form-control" id="client_extra_allowed_ips" value="{{ StringsJoin .client_defaults.ExtraAllowedIps "," }}">
</div> </div>
<div class="form-group">
<label for="client_endpoint" class="control-label">Endpoint</label>
<input type="text" class="form-control" id="client_endpoint" name="client_endpoint">
</div>
<div class="form-group"> <div class="form-group">
<div class="icheck-primary d-inline"> <div class="icheck-primary d-inline">
<input type="checkbox" id="use_server_dns" {{ if .client_defaults.UseServerDNS }}checked{{ end }}> <input type="checkbox" id="use_server_dns" {{ if .client_defaults.UseServerDNS }}checked{{ end }}>
@ -269,6 +281,14 @@
<input type="text" class="form-control" id="client_preshared_key" name="client_preshared_key" placeholder="Autogenerated - enter &quot;-&quot; to skip generation"> <input type="text" class="form-control" id="client_preshared_key" name="client_preshared_key" placeholder="Autogenerated - enter &quot;-&quot; to skip generation">
</div> </div>
</details> </details>
<details style="margin-top: 0.5rem;">
<summary><strong>Additional configuration</strong>
</summary>
<div class="form-group" style="margin-top: 0.5rem;">
<label for="client_telegram_userid" class="control-label">Telegram userid</label>
<input type="text" class="form-control" id="client_telegram_userid" name="client_telegram_userid">
</div>
</details>
</div> </div>
<div class="modal-footer justify-content-between"> <div class="modal-footer justify-content-between">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
@ -364,6 +384,36 @@
$(document).ready(function () { $(document).ready(function () {
addGlobalStyle(`
.toast-top-right-fix {
top: 67px;
right: 12px;
}
`, 'toastrToastStyleFix')
toastr.options.closeDuration = 100;
// toastr.options.timeOut = 10000;
toastr.options.positionClass = 'toast-top-right-fix';
updateApplyConfigVisibility()
// from clients.html
updateSearchList()
});
function addGlobalStyle(css, id) {
if (!document.querySelector('#' + id)) {
let head = document.head
if (!head) { return }
let style = document.createElement('style')
style.type = 'text/css'
style.id = id
style.innerHTML = css
head.appendChild(style)
}
}
function updateApplyConfigVisibility() {
$.ajax({ $.ajax({
cache: false, cache: false,
method: 'GET', method: 'GET',
@ -384,8 +434,7 @@
toastr.error(responseJson['message']); toastr.error(responseJson['message']);
} }
}); });
}
});
// populateClient function for render new client info // populateClient function for render new client info
@ -411,8 +460,10 @@
function submitNewClient() { function submitNewClient() {
const name = $("#client_name").val(); const name = $("#client_name").val();
const email = $("#client_email").val(); const email = $("#client_email").val();
const telegram_userid = $("#client_telegram_userid").val();
const allocated_ips = $("#client_allocated_ips").val().split(","); const allocated_ips = $("#client_allocated_ips").val().split(",");
const allowed_ips = $("#client_allowed_ips").val().split(","); const allowed_ips = $("#client_allowed_ips").val().split(",");
const endpoint = $("#client_endpoint").val();
let use_server_dns = false; let use_server_dns = false;
let extra_allowed_ips = []; let extra_allowed_ips = [];
@ -433,8 +484,8 @@
const public_key = $("#client_public_key").val(); const public_key = $("#client_public_key").val();
const preshared_key = $("#client_preshared_key").val(); const preshared_key = $("#client_preshared_key").val();
const data = {"name": name, "email": email, "allocated_ips": allocated_ips, "allowed_ips": allowed_ips, const data = {"name": name, "email": email, "telegram_userid": telegram_userid, "allocated_ips": allocated_ips, "allowed_ips": allowed_ips,
"extra_allowed_ips": extra_allowed_ips, "use_server_dns": use_server_dns, "enabled": enabled, "extra_allowed_ips": extra_allowed_ips, "endpoint": endpoint, "use_server_dns": use_server_dns, "enabled": enabled,
"public_key": public_key, "preshared_key": preshared_key}; "public_key": public_key, "preshared_key": preshared_key};
$.ajax({ $.ajax({
@ -451,6 +502,7 @@
if (window.location.pathname === "{{.basePath}}/") { if (window.location.pathname === "{{.basePath}}/") {
populateClient(resp.id); populateClient(resp.id);
} }
updateApplyConfigVisibility()
}, },
error: function(jqXHR, exception) { error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText); const responseJson = jQuery.parseJSON(jqXHR.responseText);
@ -461,19 +513,32 @@
// updateIPAllocationSuggestion function for automatically fill // updateIPAllocationSuggestion function for automatically fill
// the IP Allocation input with suggested ip addresses // the IP Allocation input with suggested ip addresses
function updateIPAllocationSuggestion() { function updateIPAllocationSuggestion(forceDefault = false) {
let subnetRange = $("#subnet_ranges").select2('val');
if (forceDefault || !subnetRange || subnetRange.length === 0) {
subnetRange = '__default_any__'
}
$.ajax({ $.ajax({
cache: false, cache: false,
method: 'GET', method: 'GET',
url: '{{.basePath}}/api/suggest-client-ips', url: `{{.basePath}}/api/suggest-client-ips?sr=${subnetRange}`,
dataType: 'json', dataType: 'json',
contentType: "application/json", contentType: "application/json",
success: function(data) { success: function(data) {
const allocated_ips = $("#client_allocated_ips").val().split(",");
allocated_ips.forEach(function (item, index) {
$('#client_allocated_ips').removeTag(escape(item));
})
data.forEach(function (item, index) { data.forEach(function (item, index) {
$('#client_allocated_ips').addTag(item); $('#client_allocated_ips').addTag(item);
}) })
}, },
error: function(jqXHR, exception) { error: function(jqXHR, exception) {
const allocated_ips = $("#client_allocated_ips").val().split(",");
allocated_ips.forEach(function (item, index) {
$('#client_allocated_ips').removeTag(escape(item));
})
const responseJson = jQuery.parseJSON(jqXHR.responseText); const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']); toastr.error(responseJson['message']);
} }
@ -492,6 +557,7 @@
'defaultText': 'Add More', 'defaultText': 'Add More',
'removeWithBackspace': true, 'removeWithBackspace': true,
'minChars': 0, 'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666' 'placeholderColor': '#666666'
}); });
@ -503,6 +569,7 @@
'defaultText': 'Add More', 'defaultText': 'Add More',
'removeWithBackspace': true, 'removeWithBackspace': true,
'minChars': 0, 'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666' 'placeholderColor': '#666666'
}); });
@ -513,6 +580,7 @@
'defaultText': 'Add More', 'defaultText': 'Add More',
'removeWithBackspace': true, 'removeWithBackspace': true,
'minChars': 0, 'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666' 'placeholderColor': '#666666'
}); });
@ -557,10 +625,19 @@
$("#client_preshared_key").val(""); $("#client_preshared_key").val("");
$("#client_allocated_ips").importTags(''); $("#client_allocated_ips").importTags('');
$("#client_extra_allowed_ips").importTags(''); $("#client_extra_allowed_ips").importTags('');
updateIPAllocationSuggestion(); $("#client_endpoint").val('');
$("#client_telegram_userid").val('');
updateSubnetRangesList("#subnet_ranges");
updateIPAllocationSuggestion(true);
}); });
}); });
// handle subnet range select
$('#subnet_ranges').on('select2:select', function (e) {
// console.log('Selected Option: ', $("#subnet_ranges").select2('val'));
updateIPAllocationSuggestion();
});
// apply_config_confirm button event // apply_config_confirm button event
$(document).ready(function () { $(document).ready(function () {
$("#apply_config_confirm").click(function () { $("#apply_config_confirm").click(function () {
@ -571,6 +648,7 @@
dataType: 'json', dataType: 'json',
contentType: "application/json", contentType: "application/json",
success: function(data) { success: function(data) {
updateApplyConfigVisibility()
$("#modal_apply_config").modal('hide'); $("#modal_apply_config").modal('hide');
toastr.success('Applied config successfully'); toastr.success('Applied config successfully');
}, },

View file

@ -80,6 +80,35 @@ Wireguard Clients
</div> </div>
<!-- /.modal --> <!-- /.modal -->
<div class="modal fade" id="modal_telegram_client">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Telegram Configuration</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form name="frm_telegram_client" id="frm_telegram_client">
<div class="modal-body">
<input type="hidden" id="tg_client_id" name="tg_client_id">
<div class="form-group">
<label for="tg_client_userid" class="control-label">Telegram userid</label>
<input type="text" class="form-control" id="tg_client_userid" name="tg_client_userid">
</div>
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success">Send</button>
</div>
</form>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
<div class="modal fade" id="modal_edit_client"> <div class="modal fade" id="modal_edit_client">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
@ -100,6 +129,12 @@ Wireguard Clients
<label for="_client_email" class="control-label">Email</label> <label for="_client_email" class="control-label">Email</label>
<input type="text" class="form-control" id="_client_email" name="client_email"> <input type="text" class="form-control" id="_client_email" name="client_email">
</div> </div>
<div class="form-group">
<label for="_subnet_ranges" class="control-label">Subnet range</label>
<select id="_subnet_ranges" class="select2"
data-placeholder="Select a subnet range" style="width: 100%;">
</select>
</div>
<div class="form-group"> <div class="form-group">
<label for="_client_allocated_ips" class="control-label">IP Allocation</label> <label for="_client_allocated_ips" class="control-label">IP Allocation</label>
<input type="text" data-role="tagsinput" class="form-control" id="_client_allocated_ips"> <input type="text" data-role="tagsinput" class="form-control" id="_client_allocated_ips">
@ -113,6 +148,10 @@ Wireguard Clients
<input type="text" data-role="tagsinput" class="form-control" <input type="text" data-role="tagsinput" class="form-control"
id="_client_extra_allowed_ips"> id="_client_extra_allowed_ips">
</div> </div>
<div class="form-group">
<label for="_client_endpoint" class="control-label">Endpoint</label>
<input type="text" class="form-control" id="_client_endpoint" name="client_endpoint">
</div>
<div class="form-group"> <div class="form-group">
<div class="icheck-primary d-inline"> <div class="icheck-primary d-inline">
<input type="checkbox" id="_use_server_dns"> <input type="checkbox" id="_use_server_dns">
@ -149,6 +188,14 @@ Wireguard Clients
<input type="text" class="form-control" id="_client_preshared_key" name="_client_preshared_key"> <input type="text" class="form-control" id="_client_preshared_key" name="_client_preshared_key">
</div> </div>
</details> </details>
<details style="margin-top: 0.5rem;">
<summary><strong>Additional configuration</strong>
</summary>
<div class="form-group" style="margin-top: 0.5rem;">
<label for="_client_telegram_userid" class="control-label">Telegram userid</label>
<input type="text" class="form-control" id="_client_telegram_userid" name="_client_telegram_userid">
</div>
</details>
</div> </div>
<div class="modal-footer justify-content-between"> <div class="modal-footer justify-content-between">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
@ -249,13 +296,102 @@ Wireguard Clients
setClientStatus(clientID, true); setClientStatus(clientID, true);
const divElement = document.getElementById("paused_" + clientID); const divElement = document.getElementById("paused_" + clientID);
divElement.style.visibility = "hidden"; divElement.style.visibility = "hidden";
updateApplyConfigVisibility()
} }
function pauseClient(clientID) { function pauseClient(clientID) {
setClientStatus(clientID, false); setClientStatus(clientID, false);
const divElement = document.getElementById("paused_" + clientID); const divElement = document.getElementById("paused_" + clientID);
divElement.style.visibility = "visible"; divElement.style.visibility = "visible";
updateApplyConfigVisibility()
} }
// updateIPAllocationSuggestion function for automatically fill
// the IP Allocation input with suggested ip addresses
// FOR CHANGING A SUBNET OF AN EXISTING CLIENT
function updateIPAllocationSuggestionExisting() {
let subnetRange = $("#_subnet_ranges").select2('val');
if (!subnetRange || subnetRange.length === 0) {
subnetRange = '__default_any__'
}
$.ajax({
cache: false,
method: 'GET',
url: `{{.basePath}}/api/suggest-client-ips?sr=${subnetRange}`,
dataType: 'json',
contentType: "application/json",
success: function(data) {
const allocated_ips = $("#_client_allocated_ips").val().split(",");
allocated_ips.forEach(function (item, index) {
$('#_client_allocated_ips').removeTag(escape(item));
})
data.forEach(function (item, index) {
$('#_client_allocated_ips').addTag(item);
})
},
error: function(jqXHR, exception) {
const allocated_ips = $("#_client_allocated_ips").val().split(",");
allocated_ips.forEach(function (item, index) {
$('#_client_allocated_ips').removeTag(escape(item));
})
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
function updateSubnetRangesList(elementID, preselectedVal) {
$.getJSON("{{.basePath}}/api/subnet-ranges", null, function(data) {
$(`${elementID} option`).remove();
$(elementID).append(
$("<option></option>")
.text("Any")
.val("__default_any__")
);
$.each(data, function(index, item) {
$(elementID).append(
$("<option></option>")
.text(item)
.val(item)
);
if (item === preselectedVal) {
console.log(preselectedVal);
$(elementID).val(preselectedVal).trigger('change')
}
});
});
}
function updateSearchList() {
$.getJSON("{{.basePath}}/api/subnet-ranges", null, function(data) {
$("#status-selector option").remove();
$("#status-selector").append(
$("<option></option>")
.text("All")
.val("All"),
$("<option></option>")
.text("Enabled")
.val("Enabled"),
$("<option></option>")
.text("Disabled")
.val("Disabled"),
$("<option></option>")
.text("Connected")
.val("Connected"),
$("<option></option>")
.text("Disconnected")
.val("Disconnected")
);
$.each(data, function(index, item) {
$("#status-selector").append(
$("<option></option>")
.text(item)
.val(item)
);
});
});
}
</script> </script>
<script> <script>
// load client list // load client list
@ -284,6 +420,11 @@ Wireguard Clients
} }
}) })
$(".badge-secondary").filter(':contains("' + query + '")').parent().parent().parent().show(); $(".badge-secondary").filter(':contains("' + query + '")').parent().parent().parent().show();
$(".fa-tguserid").each(function () {
if ($(this).parent().text().trim().indexOf(query.trim()) != -1) {
$(this).closest('.col-lg-4').show();
}
})
}) })
$("#status-selector").on('change', function () { $("#status-selector").on('change', function () {
@ -345,7 +486,18 @@ Wireguard Clients
}); });
break; break;
default: default:
$('.col-lg-4').show(); $('.col-lg-4').hide();
const selectedSR = $("#status-selector").val()
$(".fa-subnetrange").each(function () {
const srs = $(this).parent().text().trim().split(',')
for (const sr of srs) {
if (sr === selectedSR) {
$(this).closest('.col-lg-4').show();
break
}
}
})
// $('.col-lg-4').show();
break; break;
} }
}); });
@ -396,6 +548,7 @@ Wireguard Clients
toastr.success('Removed client successfully'); toastr.success('Removed client successfully');
const divElement = document.getElementById('client_' + client_id); const divElement = document.getElementById('client_' + client_id);
divElement.style.display = "none"; divElement.style.display = "none";
updateApplyConfigVisibility()
}, },
error: function(jqXHR, exception) { error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText); const responseJson = jQuery.parseJSON(jqXHR.responseText);
@ -423,6 +576,7 @@ Wireguard Clients
'defaultText': 'Add More', 'defaultText': 'Add More',
'removeWithBackspace': true, 'removeWithBackspace': true,
'minChars': 0, 'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666' 'placeholderColor': '#666666'
}); });
@ -434,6 +588,7 @@ Wireguard Clients
'defaultText': 'Add More', 'defaultText': 'Add More',
'removeWithBackspace': true, 'removeWithBackspace': true,
'minChars': 0, 'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666' 'placeholderColor': '#666666'
}); });
@ -444,6 +599,7 @@ Wireguard Clients
'defaultText': 'Add More', 'defaultText': 'Add More',
'removeWithBackspace' : true, 'removeWithBackspace' : true,
'minChars': 0, 'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666' 'placeholderColor': '#666666'
}) })
@ -459,9 +615,17 @@ Wireguard Clients
modal.find(".modal-title").text("Edit Client " + client.name); modal.find(".modal-title").text("Edit Client " + client.name);
modal.find("#_client_id").val(client.id); modal.find("#_client_id").val(client.id);
modal.find("#_client_telegram_userid").val(client.telegram_userid);
modal.find("#_client_name").val(client.name); modal.find("#_client_name").val(client.name);
modal.find("#_client_email").val(client.email); modal.find("#_client_email").val(client.email);
let preselectedEl
if (client.subnet_ranges && client.subnet_ranges.length > 0) {
preselectedEl = client.subnet_ranges[0]
}
updateSubnetRangesList("#_subnet_ranges", preselectedEl);
modal.find("#_client_allocated_ips").importTags(''); modal.find("#_client_allocated_ips").importTags('');
client.allocated_ips.forEach(function (obj) { client.allocated_ips.forEach(function (obj) {
modal.find("#_client_allocated_ips").addTag(obj); modal.find("#_client_allocated_ips").addTag(obj);
@ -477,11 +641,18 @@ Wireguard Clients
modal.find("#_client_extra_allowed_ips").addTag(obj); modal.find("#_client_extra_allowed_ips").addTag(obj);
}); });
modal.find("#_client_endpoint").val(client.endpoint);
modal.find("#_use_server_dns").prop("checked", client.use_server_dns); modal.find("#_use_server_dns").prop("checked", client.use_server_dns);
modal.find("#_enabled").prop("checked", client.enabled); modal.find("#_enabled").prop("checked", client.enabled);
modal.find("#_client_public_key").val(client.public_key); modal.find("#_client_public_key").val(client.public_key);
modal.find("#_client_preshared_key").val(client.preshared_key); modal.find("#_client_preshared_key").val(client.preshared_key);
// handle subnet range select
$('#_subnet_ranges').on('select2:select', function (e) {
updateIPAllocationSuggestionExisting();
});
}, },
error: function (jqXHR, exception) { error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText); const responseJson = jQuery.parseJSON(jqXHR.responseText);
@ -536,8 +707,29 @@ Wireguard Clients
success: function(resp) { success: function(resp) {
$("#modal_email_client").modal('hide'); $("#modal_email_client").modal('hide');
toastr.success('Sent email to client successfully'); toastr.success('Sent email to client successfully');
// Refresh the home page (clients page) after sending email successfully },
location.reload(); error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
// submitTelegramClient function for sending a telegram message with the configuration to the client
function submitTelegramClient() {
const client_id = $("#tg_client_id").val();
const userid = $("#tg_client_userid").val();
const data = {"id": client_id, "userid": userid};
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/send-telegram-client',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function(resp) {
$("#modal_telegram_client").modal('hide');
toastr.success('Sent config via telegram to client successfully');
}, },
error: function(jqXHR, exception) { error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText); const responseJson = jQuery.parseJSON(jqXHR.responseText);
@ -553,6 +745,7 @@ Wireguard Clients
const client_id = $("#_client_id").val(); const client_id = $("#_client_id").val();
const name = $("#_client_name").val(); const name = $("#_client_name").val();
const email = $("#_client_email").val(); const email = $("#_client_email").val();
const telegram_userid = $("#_client_telegram_userid").val();
const allocated_ips = $("#_client_allocated_ips").val().split(","); const allocated_ips = $("#_client_allocated_ips").val().split(",");
const allowed_ips = $("#_client_allowed_ips").val().split(","); const allowed_ips = $("#_client_allowed_ips").val().split(",");
let use_server_dns = false; let use_server_dns = false;
@ -564,6 +757,8 @@ Wireguard Clients
extra_allowed_ips = $("#_client_extra_allowed_ips").val().split(","); extra_allowed_ips = $("#_client_extra_allowed_ips").val().split(",");
} }
const endpoint = $("#_client_endpoint").val();
if ($("#_use_server_dns").is(':checked')){ if ($("#_use_server_dns").is(':checked')){
use_server_dns = true; use_server_dns = true;
} }
@ -574,8 +769,9 @@ Wireguard Clients
enabled = true; enabled = true;
} }
const data = {"id": client_id, "name": name, "email": email, "allocated_ips": allocated_ips, const data = {"id": client_id, "name": name, "email": email, "telegram_userid": telegram_userid, "allocated_ips": allocated_ips,
"allowed_ips": allowed_ips, "extra_allowed_ips": extra_allowed_ips, "use_server_dns": use_server_dns, "enabled": enabled, "public_key": public_key, "preshared_key": preshared_key}; "allowed_ips": allowed_ips, "extra_allowed_ips": extra_allowed_ips, "endpoint": endpoint,
"use_server_dns": use_server_dns, "enabled": enabled, "public_key": public_key, "preshared_key": preshared_key};
$.ajax({ $.ajax({
cache: false, cache: false,
@ -604,6 +800,8 @@ Wireguard Clients
submitEditClient(); submitEditClient();
} else if (formId === "frm_email_client") { } else if (formId === "frm_email_client") {
submitEmailClient(); submitEmailClient();
} else if (formId === "frm_telegram_client") {
submitTelegramClient();
} }
} }
@ -640,6 +838,30 @@ Wireguard Clients
regenerateQRCode(); regenerateQRCode();
}); });
$("#modal_telegram_client").on('show.bs.modal', function (event) {
let modal = $(this);
const button = $(event.relatedTarget);
const client_id = button.data('clientid');
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/api/client/' + client_id,
dataType: 'json',
contentType: "application/json",
success: function (resp) {
const client = resp.Client;
modal.find(".modal-title").text("Send config to client " + client.name);
modal.find("#tg_client_id").val(client.id);
modal.find("#tg_client_userid").val(client.telegram_userid);
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
$(document).ready(function () { $(document).ready(function () {
$.validator.setDefaults({ $.validator.setDefaults({
submitHandler: function (form) { submitHandler: function (form) {
@ -695,6 +917,32 @@ Wireguard Clients
$(element).removeClass('is-invalid'); $(element).removeClass('is-invalid');
} }
}); });
// Telegram client form validation
$("#frm_telegram_client").validate({
rules: {
tg_client_userid: {
required: true,
number: true,
},
},
messages: {
tg_client_userid: {
required: "Please enter a telegram userid",
number: "Please enter a valid telegram userid"
},
},
errorElement: 'span',
errorPlacement: function (error, element) {
error.addClass('invalid-feedback');
element.closest('.form-group').append(error);
},
highlight: function (element, errorClass, validClass) {
$(element).addClass('is-invalid');
},
unhighlight: function (element, errorClass, validClass) {
$(element).removeClass('is-invalid');
}
});
// //
}); });
</script> </script>

View file

@ -203,6 +203,7 @@ Global Settings
'defaultText': 'Add More', 'defaultText': 'Add More',
'removeWithBackspace': true, 'removeWithBackspace': true,
'minChars': 0, 'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666' 'placeholderColor': '#666666'
}); });

View file

@ -42,6 +42,12 @@ Wireguard Server Settings
<input type="text" class="form-control" id="post_up" name="post_up" <input type="text" class="form-control" id="post_up" name="post_up"
placeholder="Post Up Script" value="{{ .serverInterface.PostUp }}"> placeholder="Post Up Script" value="{{ .serverInterface.PostUp }}">
</div> </div>
<div class="form-group">
<label for="pre_down">Pre Down Script</label>
<input type="text" class="form-control" id="pre_down" name="pre_down"
placeholder="Pre Down Script" value="{{ .serverInterface.PreDown }}">
</div>
<div class="form-group"> <div class="form-group">
<label for="post_down">Post Down Script</label> <label for="post_down">Post Down Script</label>
<input type="text" class="form-control" id="post_down" name="post_down" <input type="text" class="form-control" id="post_down" name="post_down"
@ -130,8 +136,9 @@ Wireguard Server Settings
const addresses = $("#addresses").val().split(","); const addresses = $("#addresses").val().split(",");
const listen_port = $("#listen_port").val(); const listen_port = $("#listen_port").val();
const post_up = $("#post_up").val(); const post_up = $("#post_up").val();
const pre_down = $("#pre_down").val();
const post_down = $("#post_down").val(); const post_down = $("#post_down").val();
const data = {"addresses": addresses, "listen_port": listen_port, "post_up": post_up, "post_down": post_down}; const data = {"addresses": addresses, "listen_port": listen_port, "post_up": post_up, "pre_down": pre_down, "post_down": post_down};
$.ajax({ $.ajax({
cache: false, cache: false,
@ -160,6 +167,7 @@ Wireguard Server Settings
'defaultText': 'Add More', 'defaultText': 'Add More',
'removeWithBackspace': true, 'removeWithBackspace': true,
'minChars': 0, 'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666' 'placeholderColor': '#666666'
}); });

View file

@ -9,6 +9,7 @@ ListenPort = {{ .serverConfig.Interface.ListenPort }}
PrivateKey = {{ .serverConfig.KeyPair.PrivateKey }} PrivateKey = {{ .serverConfig.KeyPair.PrivateKey }}
{{if .globalSettings.MTU}}MTU = {{ .globalSettings.MTU }}{{end}} {{if .globalSettings.MTU}}MTU = {{ .globalSettings.MTU }}{{end}}
PostUp = {{ .serverConfig.Interface.PostUp }} PostUp = {{ .serverConfig.Interface.PostUp }}
PreDown = {{ .serverConfig.Interface.PreDown }}
PostDown = {{ .serverConfig.Interface.PostDown }} PostDown = {{ .serverConfig.Interface.PostDown }}
Table = {{ .globalSettings.Table }} Table = {{ .globalSettings.Table }}
@ -16,10 +17,12 @@ Table = {{ .globalSettings.Table }}
# ID: {{ .Client.ID }} # ID: {{ .Client.ID }}
# Name: {{ .Client.Name }} # Name: {{ .Client.Name }}
# Email: {{ .Client.Email }} # Email: {{ .Client.Email }}
# Telegram: {{ .Client.TgUserid }}
# Created at: {{ .Client.CreatedAt }} # Created at: {{ .Client.CreatedAt }}
# Update at: {{ .Client.UpdatedAt }} # Update at: {{ .Client.UpdatedAt }}
[Peer] [Peer]
PublicKey = {{ .Client.PublicKey }} PublicKey = {{ .Client.PublicKey }}
{{if .Client.PresharedKey }}PresharedKey = {{ .Client.PresharedKey }} {{if .Client.PresharedKey }}PresharedKey = {{ .Client.PresharedKey }}{{end}}
{{end}}AllowedIPs = {{$first :=true}}{{range .Client.AllocatedIPs }}{{if $first}}{{$first = false}}{{else}},{{end}}{{.}}{{end}}{{range .Client.ExtraAllowedIPs }},{{.}}{{end}} AllowedIPs = {{$first :=true}}{{range .Client.AllocatedIPs }}{{if $first}}{{$first = false}}{{else}},{{end}}{{.}}{{end}}{{range .Client.ExtraAllowedIPs }},{{.}}{{end}}
{{if .Client.Endpoint }}Endpoint = {{ .Client.Endpoint }}{{end}}
{{end}}{{end}} {{end}}{{end}}

7
util/cache.go Normal file
View file

@ -0,0 +1,7 @@
package util
import "sync"
var IPToSubnetRange = map[string]uint16{}
var TgUseridToClientID = map[int64]([]string){}
var TgUseridToClientIDMutex sync.RWMutex

View file

@ -1,25 +1,32 @@
package util package util
import "strings" import (
"net"
"strings"
"github.com/labstack/gommon/log"
)
// Runtime config // Runtime config
var ( var (
DisableLogin bool DisableLogin bool
BindAddress string BindAddress string
SmtpHostname string SmtpHostname string
SmtpPort int SmtpPort int
SmtpUsername string SmtpUsername string
SmtpPassword string SmtpPassword string
SmtpHelo string SmtpNoTLSCheck bool
SmtpNoTLSCheck bool SmtpEncryption string
SmtpEncryption string SmtpAuthType string
SmtpAuthType string SmtpHelo string
SendgridApiKey string SendgridApiKey string
EmailFrom string EmailFrom string
EmailFromName string EmailFromName string
SessionSecret []byte SessionSecret []byte
WgConfTemplate string WgConfTemplate string
BasePath string BasePath string
SubnetRanges map[string]([]*net.IPNet)
SubnetRangesOrder []string
) )
const ( const (
@ -31,12 +38,14 @@ const (
DefaultDNS = "1.1.1.1" DefaultDNS = "1.1.1.1"
DefaultMTU = 1450 DefaultMTU = 1450
DefaultPersistentKeepalive = 15 DefaultPersistentKeepalive = 15
DefaultFirewallMark = "0xca6c" // i.e. 51820 DefaultFirewallMark = "0xca6c" // i.e. 51820
DefaultTable = "auto" DefaultTable = "auto"
DefaultConfigFilePath = "/etc/wireguard/wg0.conf" DefaultConfigFilePath = "/etc/wireguard/wg0.conf"
UsernameEnvVar = "WGUI_USERNAME" UsernameEnvVar = "WGUI_USERNAME"
PasswordEnvVar = "WGUI_PASSWORD" PasswordEnvVar = "WGUI_PASSWORD"
PasswordFileEnvVar = "WGUI_PASSWORD_FILE"
PasswordHashEnvVar = "WGUI_PASSWORD_HASH" PasswordHashEnvVar = "WGUI_PASSWORD_HASH"
PasswordHashFileEnvVar = "WGUI_PASSWORD_HASH_FILE"
FaviconFilePathEnvVar = "WGUI_FAVICON_FILE_PATH" FaviconFilePathEnvVar = "WGUI_FAVICON_FILE_PATH"
EndpointAddressEnvVar = "WGUI_ENDPOINT_ADDRESS" EndpointAddressEnvVar = "WGUI_ENDPOINT_ADDRESS"
DNSEnvVar = "WGUI_DNS" DNSEnvVar = "WGUI_DNS"
@ -65,3 +74,45 @@ func ParseBasePath(basePath string) string {
} }
return basePath return basePath
} }
func ParseSubnetRanges(subnetRangesStr string) map[string]([]*net.IPNet) {
subnetRanges := map[string]([]*net.IPNet){}
if subnetRangesStr == "" {
return subnetRanges
}
cidrSet := map[string]bool{}
subnetRangesStr = strings.TrimSpace(subnetRangesStr)
subnetRangesStr = strings.Trim(subnetRangesStr, ";:,")
ranges := strings.Split(subnetRangesStr, ";")
for _, rng := range ranges {
rng = strings.TrimSpace(rng)
rngSpl := strings.Split(rng, ":")
if len(rngSpl) != 2 {
log.Warnf("Unable to parse subnet range: %v. Skipped.", rng)
continue
}
rngName := strings.TrimSpace(rngSpl[0])
subnetRanges[rngName] = make([]*net.IPNet, 0)
cidrs := strings.Split(rngSpl[1], ",")
for _, cidr := range cidrs {
cidr = strings.TrimSpace(cidr)
_, net, err := net.ParseCIDR(cidr)
if err != nil {
log.Warnf("[%v] Unable to parse CIDR: %v. Skipped.", rngName, cidr)
continue
}
if cidrSet[net.String()] {
log.Warnf("[%v] CIDR already exists: %v. Skipped.", rngName, net.String())
continue
}
cidrSet[net.String()] = true
subnetRanges[rngName] = append(subnetRanges[rngName], net)
}
if len(subnetRanges[rngName]) == 0 {
delete(subnetRanges, rngName)
} else {
SubnetRangesOrder = append(SubnetRangesOrder, rngName)
}
}
return subnetRanges
}

View file

@ -1,11 +1,10 @@
package util package util
import ( import (
"bufio"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/ngoduykhanh/wireguard-ui/store"
"golang.org/x/mod/sumdb/dirhash"
"io" "io"
"io/fs" "io/fs"
"io/ioutil" "io/ioutil"
@ -19,12 +18,23 @@ import (
"text/template" "text/template"
"time" "time"
"github.com/ngoduykhanh/wireguard-ui/store"
"github.com/ngoduykhanh/wireguard-ui/telegram"
"github.com/skip2/go-qrcode"
"golang.org/x/mod/sumdb/dirhash"
externalip "github.com/glendc/go-external-ip" externalip "github.com/glendc/go-external-ip"
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
"github.com/ngoduykhanh/wireguard-ui/model" "github.com/ngoduykhanh/wireguard-ui/model"
"github.com/sdomino/scribble" "github.com/sdomino/scribble"
) )
var qrCodeSettings = model.QRCodeSettings{
Enabled: true,
IncludeDNS: true,
IncludeMTU: true,
}
// BuildClientConfig to create wireguard client config string // BuildClientConfig to create wireguard client config string
func BuildClientConfig(client model.Client, server model.Server, setting model.GlobalSetting) string { func BuildClientConfig(client model.Client, server model.Server, setting model.GlobalSetting) string {
// Interface section // Interface section
@ -93,6 +103,15 @@ func ClientDefaultsFromEnv() model.ClientDefaults {
return clientDefaults return clientDefaults
} }
// ContainsCIDR to check if ipnet1 contains ipnet2
// https://stackoverflow.com/a/40406619/6111641
// https://go.dev/play/p/Q4J-JEN3sF
func ContainsCIDR(ipnet1, ipnet2 *net.IPNet) bool {
ones1, _ := ipnet1.Mask.Size()
ones2, _ := ipnet2.Mask.Size()
return ones1 <= ones2 && ipnet1.Contains(ipnet2.IP)
}
// ValidateCIDR to validate a network CIDR // ValidateCIDR to validate a network CIDR
func ValidateCIDR(cidr string) bool { func ValidateCIDR(cidr string) bool {
_, _, err := net.ParseCIDR(cidr) _, _, err := net.ParseCIDR(cidr)
@ -315,15 +334,32 @@ func GetBroadcastIP(n *net.IPNet) net.IP {
return broadcast return broadcast
} }
// GetBroadcastAndNetworkAddrsLookup get the ip address that can't be used with current server interfaces
func GetBroadcastAndNetworkAddrsLookup(interfaceAddresses []string) map[string]bool {
list := make(map[string]bool, 0)
for _, ifa := range interfaceAddresses {
_, net, err := net.ParseCIDR(ifa)
if err != nil {
continue
}
broadcastAddr := GetBroadcastIP(net).String()
networkAddr := net.IP.String()
list[broadcastAddr] = true
list[networkAddr] = true
}
return list
}
// GetAvailableIP get the ip address that can be allocated from an CIDR // GetAvailableIP get the ip address that can be allocated from an CIDR
func GetAvailableIP(cidr string, allocatedList []string) (string, error) { // We need interfaceAddresses to find real broadcast and network addresses
func GetAvailableIP(cidr string, allocatedList, interfaceAddresses []string) (string, error) {
ip, net, err := net.ParseCIDR(cidr) ip, net, err := net.ParseCIDR(cidr)
if err != nil { if err != nil {
return "", err return "", err
} }
broadcastAddr := GetBroadcastIP(net).String() unavailableIPs := GetBroadcastAndNetworkAddrsLookup(interfaceAddresses)
networkAddr := net.IP.String()
for ip := ip.Mask(net.Mask); net.Contains(ip); inc(ip) { for ip := ip.Mask(net.Mask); net.Contains(ip); inc(ip) {
available := true available := true
@ -334,7 +370,7 @@ func GetAvailableIP(cidr string, allocatedList []string) (string, error) {
break break
} }
} }
if available && suggestedAddr != networkAddr && suggestedAddr != broadcastAddr { if available && !unavailableIPs[suggestedAddr] {
return suggestedAddr, nil return suggestedAddr, nil
} }
} }
@ -382,6 +418,126 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip
return true, nil return true, nil
} }
// findSubnetRangeForIP to find first SR for IP, and cache the match
func findSubnetRangeForIP(cidr string) (uint16, error) {
ip, _, err := net.ParseCIDR(cidr)
if err != nil {
return 0, err
}
if srName, ok := IPToSubnetRange[ip.String()]; ok {
return srName, nil
}
for srIndex, sr := range SubnetRangesOrder {
for _, srCIDR := range SubnetRanges[sr] {
if srCIDR.Contains(ip) {
IPToSubnetRange[ip.String()] = uint16(srIndex)
return uint16(srIndex), nil
}
}
}
return 0, fmt.Errorf("Subnet range not found for this IP")
}
// FillClientSubnetRange to fill subnet ranges client belongs to, does nothing if SRs are not found
func FillClientSubnetRange(client model.ClientData) model.ClientData {
cl := *client.Client
for _, ip := range cl.AllocatedIPs {
sr, err := findSubnetRangeForIP(ip)
if err != nil {
continue
}
cl.SubnetRanges = append(cl.SubnetRanges, SubnetRangesOrder[sr])
}
return model.ClientData{
Client: &cl,
QRCode: client.QRCode,
}
}
// ValidateAndFixSubnetRanges to check if subnet ranges are valid for the server configuration
// Removes all non-valid CIDRs
func ValidateAndFixSubnetRanges(db store.IStore) error {
if len(SubnetRangesOrder) == 0 {
return nil
}
server, err := db.GetServer()
if err != nil {
return err
}
var serverSubnets []*net.IPNet
for _, addr := range server.Interface.Addresses {
addr = strings.TrimSpace(addr)
_, net, err := net.ParseCIDR(addr)
if err != nil {
return err
}
serverSubnets = append(serverSubnets, net)
}
for _, rng := range SubnetRangesOrder {
cidrs := SubnetRanges[rng]
if len(cidrs) > 0 {
newCIDRs := make([]*net.IPNet, 0)
for _, cidr := range cidrs {
valid := false
for _, serverSubnet := range serverSubnets {
if ContainsCIDR(serverSubnet, cidr) {
valid = true
break
}
}
if valid {
newCIDRs = append(newCIDRs, cidr)
} else {
log.Warnf("[%v] CIDR is outside of all server subnets: %v. Removed.", rng, cidr)
}
}
if len(newCIDRs) > 0 {
SubnetRanges[rng] = newCIDRs
} else {
delete(SubnetRanges, rng)
log.Warnf("[%v] No valid CIDRs in this subnet range. Removed.", rng)
}
}
}
return nil
}
// GetSubnetRangesString to get a formatted string, representing active subnet ranges
func GetSubnetRangesString() string {
if len(SubnetRangesOrder) == 0 {
return ""
}
strB := strings.Builder{}
for _, rng := range SubnetRangesOrder {
cidrs := SubnetRanges[rng]
if len(cidrs) > 0 {
strB.WriteString(rng)
strB.WriteString(":[")
first := true
for _, cidr := range cidrs {
if !first {
strB.WriteString(", ")
}
strB.WriteString(cidr.String())
first = false
}
strB.WriteString("] ")
}
}
return strings.TrimSpace(strB.String())
}
// WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf // WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
func WriteWireGuardServerConfig(tmplDir fs.FS, serverConfig model.Server, clientDataList []model.ClientData, usersList []model.User, globalSettings model.GlobalSetting) error { func WriteWireGuardServerConfig(tmplDir fs.FS, serverConfig model.Server, clientDataList []model.ClientData, usersList []model.User, globalSettings model.GlobalSetting) error {
var tmplWireguardConf string var tmplWireguardConf string
@ -430,6 +586,57 @@ func WriteWireGuardServerConfig(tmplDir fs.FS, serverConfig model.Server, client
return nil return nil
} }
// SendRequestedConfigsToTelegram to send client all their configs. Returns failed configs list.
func SendRequestedConfigsToTelegram(db store.IStore, userid int64) []string {
failedList := make([]string, 0)
TgUseridToClientIDMutex.RLock()
if clids, found := TgUseridToClientID[userid]; found && len(clids) > 0 {
TgUseridToClientIDMutex.RUnlock()
for _, clid := range clids {
clientData, err := db.GetClientByID(clid, qrCodeSettings)
if err != nil {
// return fmt.Errorf("unable to get client")
failedList = append(failedList, clid)
continue
}
// build config
server, _ := db.GetServer()
globalSettings, _ := db.GetGlobalSettings()
config := BuildClientConfig(*clientData.Client, server, globalSettings)
configData := []byte(config)
var qrData []byte
if clientData.Client.PrivateKey != "" {
qrData, err = qrcode.Encode(config, qrcode.Medium, 512)
if err != nil {
// return fmt.Errorf("unable to encode qr")
failedList = append(failedList, clientData.Client.Name)
continue
}
}
userid, err := strconv.ParseInt(clientData.Client.TgUserid, 10, 64)
if err != nil {
// return fmt.Errorf("tg usrid is unreadable")
failedList = append(failedList, clientData.Client.Name)
continue
}
err = telegram.SendConfig(userid, clientData.Client.Name, configData, qrData, true)
if err != nil {
failedList = append(failedList, clientData.Client.Name)
continue
}
time.Sleep(2 * time.Second)
}
} else {
TgUseridToClientIDMutex.RUnlock()
}
return failedList
}
func LookupEnvOrString(key string, defaultVal string) string { func LookupEnvOrString(key string, defaultVal string) string {
if val, ok := os.LookupEnv(key); ok { if val, ok := os.LookupEnv(key); ok {
return val return val
@ -466,6 +673,20 @@ func LookupEnvOrStrings(key string, defaultVal []string) []string {
return defaultVal return defaultVal
} }
func LookupEnvOrFile(key string, defaultVal string) string {
if val, ok := os.LookupEnv(key); ok {
if file, err := os.Open(val); err == nil {
var content string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
content += scanner.Text()
}
return content
}
}
return defaultVal
}
func StringFromEmbedFile(embed fs.FS, filename string) (string, error) { func StringFromEmbedFile(embed fs.FS, filename string) (string, error) {
file, err := embed.Open(filename) file, err := embed.Open(filename)
if err != nil { if err != nil {
@ -540,3 +761,70 @@ func RandomString(length int) string {
} }
return string(b) return string(b)
} }
func ManagePerms(path string) error {
err := os.Chmod(path, 0600)
return err
}
func AddTgToClientID(userid int64, clientID string) {
TgUseridToClientIDMutex.Lock()
defer TgUseridToClientIDMutex.Unlock()
if _, ok := TgUseridToClientID[userid]; ok && TgUseridToClientID[userid] != nil {
TgUseridToClientID[userid] = append(TgUseridToClientID[userid], clientID)
} else {
TgUseridToClientID[userid] = []string{clientID}
}
}
func UpdateTgToClientID(userid int64, clientID string) {
TgUseridToClientIDMutex.Lock()
defer TgUseridToClientIDMutex.Unlock()
// Detach clientID from any existing userid
for uid, cls := range TgUseridToClientID {
if cls != nil {
filtered := filterStringSlice(cls, clientID)
if len(filtered) > 0 {
TgUseridToClientID[uid] = filtered
} else {
delete(TgUseridToClientID, uid)
}
}
}
// Attach it to the new one
if _, ok := TgUseridToClientID[userid]; ok && TgUseridToClientID[userid] != nil {
TgUseridToClientID[userid] = append(TgUseridToClientID[userid], clientID)
} else {
TgUseridToClientID[userid] = []string{clientID}
}
}
func RemoveTgToClientID(clientID string) {
TgUseridToClientIDMutex.Lock()
defer TgUseridToClientIDMutex.Unlock()
// Detach clientID from any existing userid
for uid, cls := range TgUseridToClientID {
if cls != nil {
filtered := filterStringSlice(cls, clientID)
if len(filtered) > 0 {
TgUseridToClientID[uid] = filtered
} else {
delete(TgUseridToClientID, uid)
}
}
}
}
func filterStringSlice(s []string, excludedStr string) []string {
filtered := s[:0]
for _, v := range s {
if v != excludedStr {
filtered = append(filtered, v)
}
}
return filtered
}