mirror of
https://github.com/ngoduykhanh/wireguard-ui.git
synced 2025-04-19 19:59:13 +03:00
Resolve conflict
This commit is contained in:
commit
8cfe9a3d5b
25 changed files with 1550 additions and 405 deletions
8
.github/workflows/docker-build.yml
vendored
8
.github/workflows/docker-build.yml
vendored
|
@ -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
3
.gitignore
vendored
|
@ -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
67
CONTRIBUTING.md
Normal 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.
|
|
@ -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
|
||||||
|
|
19
README.md
19
README.md
|
@ -37,14 +37,18 @@ 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 |
|
||||||
|
| `SESSION_SECRET_FILE` | Optional filepath for the secret key used to encrypt the session cookies. Leave `SESSION_SECRET` blank to take effect | N/A |
|
||||||
|
| `SUBNET_RANGES` | The list of address subdivision ranges. Format: `SR Name:10.0.1.0/24; SR2:10.0.2.0/24,10.0.3.0/24` Each CIDR must be inside one of the server interfaces. | N/A |
|
||||||
| `WGUI_USERNAME` | The username for the login page. Used for db initialization only | `admin` |
|
| `WGUI_USERNAME` | The username for the login page. Used for db initialization only | `admin` |
|
||||||
| `WGUI_PASSWORD` | The password for the user on the login page. Will be hashed automatically. Used for db initialization only | `admin` |
|
| `WGUI_PASSWORD` | The password for the user on the login page. Will be hashed automatically. Used for db initialization only | `admin` |
|
||||||
|
| `WGUI_PASSWORD_FILE` | Optional filepath for the user login password. Will be hashed automatically. Used for db initialization only. Leave `WGUI_PASSWORD` blank to take effect | N/A |
|
||||||
| `WGUI_PASSWORD_HASH` | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only | N/A |
|
| `WGUI_PASSWORD_HASH` | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only | N/A |
|
||||||
| `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings where clients should connect to | Resolved to your public ip address |
|
| `WGUI_PASSWORD_HASH_FILE` | Optional filepath for the user login password hash. (alternative to `WGUI_PASSWORD_FILE`). Used for db initialization only. Leave `WGUI_PASSWORD_HASH` blank to take effect | N/A |
|
||||||
|
| `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings where clients should connect to. The endpoint can contain a port as well, useful when you are listening internally on the `WGUI_SERVER_LISTEN_PORT` port, but you forward on another port (ex 9000). Ex: myvpn.dyndns.com:9000 | Resolved to your public ip address |
|
||||||
| `WGUI_FAVICON_FILE_PATH` | The file path used as website favicon | Embedded WireGuard logo |
|
| `WGUI_FAVICON_FILE_PATH` | The file path used as website favicon | Embedded WireGuard logo |
|
||||||
| `WGUI_DNS` | The default DNS servers (comma-separated-list) used in the global settings | `1.1.1.1` |
|
| `WGUI_DNS` | The default DNS servers (comma-separated-list) used in the global settings | `1.1.1.1` |
|
||||||
| `WGUI_MTU` | The default MTU used in global settings | `1450` |
|
| `WGUI_MTU` | The default MTU used in global settings | `1450` |
|
||||||
|
@ -57,13 +61,18 @@ docker-compose up
|
||||||
| `EMAIL_FROM_ADDRESS` | The sender email address | N/A |
|
| `EMAIL_FROM_ADDRESS` | The sender email address | N/A |
|
||||||
| `EMAIL_FROM_NAME` | The sender name | `WireGuard UI` |
|
| `EMAIL_FROM_NAME` | The sender name | `WireGuard UI` |
|
||||||
| `SENDGRID_API_KEY` | The SendGrid api key | N/A |
|
| `SENDGRID_API_KEY` | The SendGrid api key | N/A |
|
||||||
|
| `SENDGRID_API_KEY_FILE` | Optional filepath for the SendGrid api key. Leave `SENDGRID_API_KEY` blank to take effect | N/A |
|
||||||
| `SMTP_HOSTNAME` | The SMTP IP address or hostname | `127.0.0.1` |
|
| `SMTP_HOSTNAME` | The SMTP IP address or hostname | `127.0.0.1` |
|
||||||
| `SMTP_PORT` | The SMTP port | `25` |
|
| `SMTP_PORT` | The SMTP port | `25` |
|
||||||
| `SMTP_USERNAME` | The SMTP username | N/A |
|
| `SMTP_USERNAME` | The SMTP username | N/A |
|
||||||
| `SMTP_PASSWORD` | The SMTP user password | 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_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_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` |
|
| `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
|
||||||
|
|
||||||
|
|
|
@ -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> `;
|
allowedIpsHtml += `<small class="badge badge-secondary">${obj}</small> `;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let subnetRangesString = "";
|
||||||
|
if (obj.Client.subnet_ranges && obj.Client.subnet_ranges.length > 0) {
|
||||||
|
subnetRangesString = obj.Client.subnet_ranges.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
// 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>
|
||||||
|
|
|
@ -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
63
go.mod
|
@ -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
314
go.sum
|
@ -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=
|
|
||||||
|
|
|
@ -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,6 +382,11 @@ 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,
|
||||||
|
@ -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,6 +523,10 @@ func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailCon
|
||||||
c.Bind(&payload)
|
c.Bind(&payload)
|
||||||
// TODO validate email
|
// TODO validate email
|
||||||
|
|
||||||
|
if _, err := xid.FromString(payload.ID); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid client ID"})
|
||||||
|
}
|
||||||
|
|
||||||
qrCodeSettings := model.QRCodeSettings{
|
qrCodeSettings := model.QRCodeSettings{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
IncludeDNS: true,
|
IncludeDNS: true,
|
||||||
|
@ -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,20 +1085,46 @@ 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)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
if strings.Contains(ip, ":") {
|
||||||
|
ipSet[fmt.Sprintf("%s/128", ip)] = struct{}{}
|
||||||
|
} else {
|
||||||
|
ipSet[fmt.Sprintf("%s/32", ip)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
|
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
|
||||||
false,
|
false,
|
||||||
fmt.Sprintf("Cannot suggest ip allocation: failed to get available ip from network %s", cidr),
|
"Cannot suggest ip allocation: failed to get available ip. Try a different subnet or deallocate some ips.",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if strings.Contains(ip, ":") {
|
|
||||||
suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/128", ip))
|
for ip := range ipSet {
|
||||||
} else {
|
suggestedIPs = append(suggestedIPs, ip)
|
||||||
suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/32", 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 {
|
||||||
|
|
129
main.go
129
main.go
|
@ -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"
|
||||||
|
@ -30,18 +35,22 @@ var (
|
||||||
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"
|
||||||
|
flagTelegramToken string
|
||||||
|
flagTelegramAllowConfRequest bool = false
|
||||||
|
flagTelegramFloodWait int = 60
|
||||||
flagSessionSecret string = util.RandomString(32)
|
flagSessionSecret string = util.RandomString(32)
|
||||||
flagWgConfTemplate string
|
flagWgConfTemplate string
|
||||||
flagBasePath 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)))
|
||||||
|
|
||||||
|
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))
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
@ -127,27 +140,45 @@ func (o *JsonDB) Init() error {
|
||||||
user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername)
|
user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername)
|
||||||
user.Admin = util.DefaultIsAdmin
|
user.Admin = util.DefaultIsAdmin
|
||||||
user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "")
|
user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "")
|
||||||
|
if user.PasswordHash == "" {
|
||||||
|
user.PasswordHash = util.LookupEnvOrFile(util.PasswordHashFileEnvVar, "")
|
||||||
if user.PasswordHash == "" {
|
if user.PasswordHash == "" {
|
||||||
plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword)
|
plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword)
|
||||||
|
if plaintext == util.DefaultPassword {
|
||||||
|
plaintext = util.LookupEnvOrFile(util.PasswordFileEnvVar, util.DefaultPassword)
|
||||||
|
}
|
||||||
hash, err := util.HashPassword(plaintext)
|
hash, err := util.HashPassword(plaintext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
155
telegram/bot.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 "-" to skip generation">
|
<input type="text" class="form-control" id="client_preshared_key" name="client_preshared_key" placeholder="Autogenerated - enter "-" to skip generation">
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
<details style="margin-top: 0.5rem;">
|
||||||
|
<summary><strong>Additional configuration</strong>
|
||||||
|
</summary>
|
||||||
|
<div class="form-group" style="margin-top: 0.5rem;">
|
||||||
|
<label for="client_telegram_userid" class="control-label">Telegram userid</label>
|
||||||
|
<input type="text" class="form-control" id="client_telegram_userid" name="client_telegram_userid">
|
||||||
|
</div>
|
||||||
|
</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');
|
||||||
},
|
},
|
||||||
|
|
|
@ -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">×</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>
|
||||||
|
|
|
@ -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'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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
7
util/cache.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
var IPToSubnetRange = map[string]uint16{}
|
||||||
|
var TgUseridToClientID = map[int64]([]string){}
|
||||||
|
var TgUseridToClientIDMutex sync.RWMutex
|
|
@ -1,6 +1,11 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/labstack/gommon/log"
|
||||||
|
)
|
||||||
|
|
||||||
// Runtime config
|
// Runtime config
|
||||||
var (
|
var (
|
||||||
|
@ -10,16 +15,18 @@ var (
|
||||||
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 (
|
||||||
|
@ -36,7 +43,9 @@ const (
|
||||||
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
|
||||||
|
}
|
||||||
|
|
300
util/util.go
300
util/util.go
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue