mirror of
https://github.com/ngoduykhanh/wireguard-ui.git
synced 2025-05-03 22:02:23 +03:00
Telegram support (#488)
This commit is contained in:
parent
841db62347
commit
41bf0bc92c
14 changed files with 588 additions and 32 deletions
|
@ -69,6 +69,9 @@ docker-compose up
|
||||||
| `SMTP_PASSWORD_FILE` | Optional filepath for the SMTP user password. Leave `SMTP_PASSWORD` blank to take effect | 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` |
|
||||||
|
| `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) {
|
||||||
|
@ -43,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"
|
||||||
|
@ -65,6 +80,7 @@ function renderClientList(data) {
|
||||||
<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>
|
<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>
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -3,6 +3,7 @@ module github.com/ngoduykhanh/wireguard-ui
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/NicoNex/echotron/v3 v3.27.0
|
||||||
github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd
|
github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd
|
||||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||||
github.com/gorilla/sessions v1.2.0
|
github.com/gorilla/sessions v1.2.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -1,4 +1,6 @@
|
||||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
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/NicoNex/echotron/v3 v3.27.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/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/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -18,12 +19,14 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -385,9 +388,9 @@ func GetClient(db store.IStore) echo.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
qrCodeSettings := model.QRCodeSettings{
|
qrCodeSettings := model.QRCodeSettings{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
IncludeDNS: true,
|
IncludeDNS: true,
|
||||||
IncludeMTU: true,
|
IncludeMTU: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
clientData, err := db.GetClientByID(clientID, qrCodeSettings)
|
clientData, err := db.GetClientByID(clientID, qrCodeSettings)
|
||||||
|
@ -406,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 {
|
||||||
|
@ -517,9 +528,9 @@ func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailCon
|
||||||
}
|
}
|
||||||
|
|
||||||
qrCodeSettings := model.QRCodeSettings{
|
qrCodeSettings := model.QRCodeSettings{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
IncludeDNS: true,
|
IncludeDNS: true,
|
||||||
IncludeMTU: true,
|
IncludeMTU: true,
|
||||||
}
|
}
|
||||||
clientData, err := db.GetClientByID(payload.ID, qrCodeSettings)
|
clientData, err := db.GetClientByID(payload.ID, qrCodeSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -560,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 {
|
||||||
|
@ -577,6 +633,14 @@ func UpdateClient(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"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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{
|
||||||
|
@ -644,6 +708,7 @@ 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
|
||||||
|
@ -1115,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 {
|
||||||
|
|
67
main.go
67
main.go
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/gommon/log"
|
"github.com/labstack/gommon/log"
|
||||||
"github.com/ngoduykhanh/wireguard-ui/store"
|
"github.com/ngoduykhanh/wireguard-ui/store"
|
||||||
|
"github.com/ngoduykhanh/wireguard-ui/telegram"
|
||||||
|
|
||||||
"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,22 +31,25 @@ var (
|
||||||
gitRef = "N/A"
|
gitRef = "N/A"
|
||||||
buildTime = fmt.Sprintf(time.Now().UTC().Format("01-02-2006 15:04:05"))
|
buildTime = fmt.Sprintf(time.Now().UTC().Format("01-02-2006 15:04:05"))
|
||||||
// configuration variables
|
// configuration variables
|
||||||
flagDisableLogin bool = false
|
flagDisableLogin bool = false
|
||||||
flagBindAddress string = "0.0.0.0:5000"
|
flagBindAddress string = "0.0.0.0:5000"
|
||||||
flagSmtpHostname string = "127.0.0.1"
|
flagSmtpHostname string = "127.0.0.1"
|
||||||
flagSmtpPort int = 25
|
flagSmtpPort int = 25
|
||||||
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"
|
||||||
flagSendgridApiKey string
|
flagSendgridApiKey string
|
||||||
flagEmailFrom string
|
flagEmailFrom string
|
||||||
flagEmailFromName string = "WireGuard UI"
|
flagEmailFromName string = "WireGuard UI"
|
||||||
flagSessionSecret string = util.RandomString(32)
|
flagTelegramToken string
|
||||||
flagWgConfTemplate string
|
flagTelegramAllowConfRequest bool = false
|
||||||
flagBasePath string
|
flagTelegramFloodWait int = 60
|
||||||
flagSubnetRanges string
|
flagSessionSecret string = util.RandomString(32)
|
||||||
|
flagWgConfTemplate string
|
||||||
|
flagBasePath string
|
||||||
|
flagSubnetRanges string
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -80,6 +84,9 @@ func init() {
|
||||||
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(&flagEmailFrom, "email-from", util.LookupEnvOrString("EMAIL_FROM_ADDRESS", flagEmailFrom), "'From' email address.")
|
flag.StringVar(&flagEmailFrom, "email-from", util.LookupEnvOrString("EMAIL_FROM_ADDRESS", flagEmailFrom), "'From' email address.")
|
||||||
flag.StringVar(&flagEmailFromName, "email-from-name", util.LookupEnvOrString("EMAIL_FROM_NAME", flagEmailFromName), "'From' email name.")
|
flag.StringVar(&flagEmailFromName, "email-from-name", util.LookupEnvOrString("EMAIL_FROM_NAME", flagEmailFromName), "'From' email name.")
|
||||||
|
flag.StringVar(&flagTelegramToken, "telegram-token", util.LookupEnvOrString("TELEGRAM_TOKEN", flagTelegramToken), "Telegram bot token for distributing configs to clients.")
|
||||||
|
flag.BoolVar(&flagTelegramAllowConfRequest, "telegram-allow-conf-request", util.LookupEnvOrBool("TELEGRAM_ALLOW_CONF_REQUEST", flagTelegramAllowConfRequest), "Allow users to get configs from the bot by sending a message.")
|
||||||
|
flag.IntVar(&flagTelegramFloodWait, "telegram-flood-wait", util.LookupEnvOrInt("TELEGRAM_FLOOD_WAIT", flagTelegramFloodWait), "Time in minutes before the next conf request is processed.")
|
||||||
flag.StringVar(&flagWgConfTemplate, "wg-conf-template", util.LookupEnvOrString("WG_CONF_TEMPLATE", flagWgConfTemplate), "Path to custom wg.conf template.")
|
flag.StringVar(&flagWgConfTemplate, "wg-conf-template", util.LookupEnvOrString("WG_CONF_TEMPLATE", flagWgConfTemplate), "Path to custom wg.conf template.")
|
||||||
flag.StringVar(&flagBasePath, "base-path", util.LookupEnvOrString("BASE_PATH", flagBasePath), "The base path of the URL")
|
flag.StringVar(&flagBasePath, "base-path", util.LookupEnvOrString("BASE_PATH", flagBasePath), "The base path of the URL")
|
||||||
flag.StringVar(&flagSubnetRanges, "subnet-ranges", util.LookupEnvOrString("SUBNET_RANGES", flagSubnetRanges), "IP ranges to choose from when assigning an IP for a client.")
|
flag.StringVar(&flagSubnetRanges, "subnet-ranges", util.LookupEnvOrString("SUBNET_RANGES", flagSubnetRanges), "IP ranges to choose from when assigning an IP for a client.")
|
||||||
|
@ -131,8 +138,15 @@ func init() {
|
||||||
util.BasePath = util.ParseBasePath(flagBasePath)
|
util.BasePath = util.ParseBasePath(flagBasePath)
|
||||||
util.SubnetRanges = util.ParseSubnetRanges(flagSubnetRanges)
|
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)
|
||||||
|
@ -221,6 +235,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)
|
||||||
|
@ -248,6 +263,13 @@ func main() {
|
||||||
// serves other static files
|
// serves other static files
|
||||||
app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler)))
|
app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler)))
|
||||||
|
|
||||||
|
initDeps := telegram.TgBotInitDependencies{
|
||||||
|
DB: db,
|
||||||
|
SendRequestedConfigsToTelegram: util.SendRequestedConfigsToTelegram,
|
||||||
|
}
|
||||||
|
|
||||||
|
initTelegram(initDeps)
|
||||||
|
|
||||||
if strings.HasPrefix(util.BindAddress, "unix://") {
|
if strings.HasPrefix(util.BindAddress, "unix://") {
|
||||||
// Listen on unix domain socket.
|
// Listen on unix domain socket.
|
||||||
// https://github.com/labstack/echo/issues/830
|
// https://github.com/labstack/echo/issues/830
|
||||||
|
@ -296,3 +318,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,6 +11,7 @@ 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"`
|
SubnetRanges []string `json:"subnet_ranges,omitempty"`
|
||||||
AllocatedIPs []string `json:"allocated_ips"`
|
AllocatedIPs []string `json:"allocated_ips"`
|
||||||
|
@ -30,7 +31,7 @@ type ClientData struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type QRCodeSettings struct {
|
type QRCodeSettings struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
IncludeDNS bool
|
IncludeDNS bool
|
||||||
IncludeMTU bool
|
IncludeMTU bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sdomino/scribble"
|
"github.com/sdomino/scribble"
|
||||||
|
@ -161,6 +162,20 @@ func (o *JsonDB) Init() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,6 +329,17 @@ 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)
|
||||||
|
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)
|
err := util.ManagePerms(clientPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -322,6 +348,7 @@ func (o *JsonDB) SaveClient(client model.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -281,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>
|
||||||
|
@ -452,6 +460,7 @@
|
||||||
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();
|
const endpoint = $("#client_endpoint").val();
|
||||||
|
@ -475,7 +484,7 @@
|
||||||
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, "endpoint": endpoint, "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};
|
||||||
|
|
||||||
|
@ -616,6 +625,8 @@
|
||||||
$("#client_preshared_key").val("");
|
$("#client_preshared_key").val("");
|
||||||
$("#client_allocated_ips").importTags('');
|
$("#client_allocated_ips").importTags('');
|
||||||
$("#client_extra_allowed_ips").importTags('');
|
$("#client_extra_allowed_ips").importTags('');
|
||||||
|
$("#client_endpoint").val('');
|
||||||
|
$("#client_telegram_userid").val('');
|
||||||
updateSubnetRangesList("#subnet_ranges");
|
updateSubnetRangesList("#subnet_ranges");
|
||||||
updateIPAllocationSuggestion(true);
|
updateIPAllocationSuggestion(true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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">
|
||||||
|
@ -159,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>
|
||||||
|
@ -383,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 () {
|
||||||
|
@ -573,6 +615,7 @@ 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);
|
||||||
|
|
||||||
|
@ -664,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);
|
||||||
|
@ -681,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;
|
||||||
|
@ -704,7 +769,7 @@ 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, "endpoint": endpoint,
|
"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};
|
"use_server_dns": use_server_dns, "enabled": enabled, "public_key": public_key, "preshared_key": preshared_key};
|
||||||
|
|
||||||
|
@ -735,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -771,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) {
|
||||||
|
@ -826,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>
|
||||||
|
|
|
@ -17,6 +17,7 @@ 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]
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
var IPToSubnetRange = map[string]uint16{}
|
var IPToSubnetRange = map[string]uint16{}
|
||||||
|
var TgUseridToClientID = map[int64]([]string){}
|
||||||
|
var TgUseridToClientIDMutex sync.RWMutex
|
||||||
|
|
121
util/util.go
121
util/util.go
|
@ -19,6 +19,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ngoduykhanh/wireguard-ui/store"
|
"github.com/ngoduykhanh/wireguard-ui/store"
|
||||||
|
"github.com/ngoduykhanh/wireguard-ui/telegram"
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
"golang.org/x/mod/sumdb/dirhash"
|
"golang.org/x/mod/sumdb/dirhash"
|
||||||
|
|
||||||
externalip "github.com/glendc/go-external-ip"
|
externalip "github.com/glendc/go-external-ip"
|
||||||
|
@ -27,6 +29,12 @@ import (
|
||||||
"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
|
||||||
|
@ -578,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
|
||||||
|
@ -707,3 +766,65 @@ func ManagePerms(path string) error {
|
||||||
err := os.Chmod(path, 0600)
|
err := os.Chmod(path, 0600)
|
||||||
return err
|
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