Skip to content

Commit

Permalink
Telegram support (ngoduykhanh#488)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xCA authored Dec 29, 2023
1 parent 841db62 commit 41bf0bc
Show file tree
Hide file tree
Showing 14 changed files with 588 additions and 32 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_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` |
| `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

Expand Down
18 changes: 17 additions & 1 deletion custom/js/helper.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
function renderClientList(data) {
$.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
let clientStatusHtml = '>'
if (obj.Client.enabled) {
Expand Down Expand Up @@ -43,7 +58,7 @@ function renderClientList(data) {
data-target="#modal_email_client" data-clientid="${obj.Client.id}"
data-clientname="${obj.Client.name}">Email</button>
</div>
${telegramButton}
<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 dropdown-toggle dropdown-icon"
Expand All @@ -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" 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-clock"></i>
${prettyDateTime(obj.Client.created_at)}</span>
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/ngoduykhanh/wireguard-ui
go 1.16

require (
github.com/NicoNex/echotron/v3 v3.27.0
github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/gorilla/sessions v1.2.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
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/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=
Expand Down
78 changes: 71 additions & 7 deletions handler/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"

Expand All @@ -18,12 +19,14 @@ import (
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
"github.com/rs/xid"
"github.com/skip2/go-qrcode"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"

"github.com/ngoduykhanh/wireguard-ui/emailer"
"github.com/ngoduykhanh/wireguard-ui/model"
"github.com/ngoduykhanh/wireguard-ui/store"
"github.com/ngoduykhanh/wireguard-ui/telegram"
"github.com/ngoduykhanh/wireguard-ui/util"
)

Expand Down Expand Up @@ -385,9 +388,9 @@ func GetClient(db store.IStore) echo.HandlerFunc {
}

qrCodeSettings := model.QRCodeSettings{
Enabled: true,
IncludeDNS: true,
IncludeMTU: true,
Enabled: true,
IncludeDNS: true,
IncludeMTU: true,
}

clientData, err := db.GetClientByID(clientID, qrCodeSettings)
Expand All @@ -406,6 +409,14 @@ func NewClient(db store.IStore) echo.HandlerFunc {
var client model.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
server, err := db.GetServer()
if err != nil {
Expand Down Expand Up @@ -517,9 +528,9 @@ func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailCon
}

qrCodeSettings := model.QRCodeSettings{
Enabled: true,
IncludeDNS: true,
IncludeMTU: true,
Enabled: true,
IncludeDNS: true,
IncludeMTU: true,
}
clientData, err := db.GetClientByID(payload.ID, qrCodeSettings)
if err != nil {
Expand Down Expand Up @@ -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
func UpdateClient(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
Expand All @@ -577,6 +633,14 @@ func UpdateClient(db store.IStore) echo.HandlerFunc {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"})
}

// Validate Telegram userid if provided
if _client.TgUserid != "" {
idNum, err := strconv.ParseInt(_client.TgUserid, 10, 64)
if err != nil || idNum == 0 {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Telegram userid must be a non-zero number"})
}
}

server, err := db.GetServer()
if err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{
Expand Down Expand Up @@ -644,6 +708,7 @@ func UpdateClient(db store.IStore) echo.HandlerFunc {
// map new data
client.Name = _client.Name
client.Email = _client.Email
client.TgUserid = _client.TgUserid
client.Enabled = _client.Enabled
client.UseServerDNS = _client.UseServerDNS
client.AllocatedIPs = _client.AllocatedIPs
Expand Down Expand Up @@ -1115,7 +1180,6 @@ func ApplyServerConfig(db store.IStore, tmplDir fs.FS) echo.HandlerFunc {
}
}


// GetHashesChanges handler returns if database hashes have changed
func GetHashesChanges(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
Expand Down
67 changes: 50 additions & 17 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
"github.com/ngoduykhanh/wireguard-ui/store"
"github.com/ngoduykhanh/wireguard-ui/telegram"

"github.com/ngoduykhanh/wireguard-ui/emailer"
"github.com/ngoduykhanh/wireguard-ui/handler"
Expand All @@ -30,22 +31,25 @@ var (
gitRef = "N/A"
buildTime = fmt.Sprintf(time.Now().UTC().Format("01-02-2006 15:04:05"))
// configuration variables
flagDisableLogin bool = false
flagBindAddress string = "0.0.0.0:5000"
flagSmtpHostname string = "127.0.0.1"
flagSmtpPort int = 25
flagSmtpUsername string
flagSmtpPassword string
flagSmtpAuthType string = "NONE"
flagSmtpNoTLSCheck bool = false
flagSmtpEncryption string = "STARTTLS"
flagSendgridApiKey string
flagEmailFrom string
flagEmailFromName string = "WireGuard UI"
flagSessionSecret string = util.RandomString(32)
flagWgConfTemplate string
flagBasePath string
flagSubnetRanges string
flagDisableLogin bool = false
flagBindAddress string = "0.0.0.0:5000"
flagSmtpHostname string = "127.0.0.1"
flagSmtpPort int = 25
flagSmtpUsername string
flagSmtpPassword string
flagSmtpAuthType string = "NONE"
flagSmtpNoTLSCheck bool = false
flagSmtpEncryption string = "STARTTLS"
flagSendgridApiKey string
flagEmailFrom string
flagEmailFromName string = "WireGuard UI"
flagTelegramToken string
flagTelegramAllowConfRequest bool = false
flagTelegramFloodWait int = 60
flagSessionSecret string = util.RandomString(32)
flagWgConfTemplate string
flagBasePath string
flagSubnetRanges string
)

const (
Expand Down Expand Up @@ -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(&flagEmailFrom, "email-from", util.LookupEnvOrString("EMAIL_FROM_ADDRESS", flagEmailFrom), "'From' email address.")
flag.StringVar(&flagEmailFromName, "email-from-name", util.LookupEnvOrString("EMAIL_FROM_NAME", flagEmailFromName), "'From' email name.")
flag.StringVar(&flagTelegramToken, "telegram-token", util.LookupEnvOrString("TELEGRAM_TOKEN", flagTelegramToken), "Telegram bot token for distributing configs to clients.")
flag.BoolVar(&flagTelegramAllowConfRequest, "telegram-allow-conf-request", util.LookupEnvOrBool("TELEGRAM_ALLOW_CONF_REQUEST", flagTelegramAllowConfRequest), "Allow users to get configs from the bot by sending a message.")
flag.IntVar(&flagTelegramFloodWait, "telegram-flood-wait", util.LookupEnvOrInt("TELEGRAM_FLOOD_WAIT", flagTelegramFloodWait), "Time in minutes before the next conf request is processed.")
flag.StringVar(&flagWgConfTemplate, "wg-conf-template", util.LookupEnvOrString("WG_CONF_TEMPLATE", flagWgConfTemplate), "Path to custom wg.conf template.")
flag.StringVar(&flagBasePath, "base-path", util.LookupEnvOrString("BASE_PATH", flagBasePath), "The base path of the URL")
flag.StringVar(&flagSubnetRanges, "subnet-ranges", util.LookupEnvOrString("SUBNET_RANGES", flagSubnetRanges), "IP ranges to choose from when assigning an IP for a client.")
Expand Down Expand Up @@ -131,8 +138,15 @@ func init() {
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
if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO {
if lvl <= log.INFO {
// print app information
fmt.Println("Wireguard UI")
fmt.Println("App Version\t:", appVersion)
Expand Down Expand Up @@ -221,6 +235,7 @@ func main() {
app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/update-client", handler.UpdateClient(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/send-telegram-client", handler.SendTelegramClient(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson)
app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession)
Expand Down Expand Up @@ -248,6 +263,13 @@ func main() {
// serves other static files
app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler)))

initDeps := telegram.TgBotInitDependencies{
DB: db,
SendRequestedConfigsToTelegram: util.SendRequestedConfigsToTelegram,
}

initTelegram(initDeps)

if strings.HasPrefix(util.BindAddress, "unix://") {
// Listen on unix domain socket.
// https://github.com/labstack/echo/issues/830
Expand Down Expand Up @@ -296,3 +318,14 @@ func initServerConfig(db store.IStore, tmplDir fs.FS) {
log.Fatalf("Cannot create server config: ", err)
}
}

func initTelegram(initDeps telegram.TgBotInitDependencies) {
go func() {
for {
err := telegram.Start(initDeps)
if err == nil {
break
}
}
}()
}
7 changes: 4 additions & 3 deletions model/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Client struct {
PublicKey string `json:"public_key"`
PresharedKey string `json:"preshared_key"`
Name string `json:"name"`
TgUserid string `json:"telegram_userid"`
Email string `json:"email"`
SubnetRanges []string `json:"subnet_ranges,omitempty"`
AllocatedIPs []string `json:"allocated_ips"`
Expand All @@ -30,7 +31,7 @@ type ClientData struct {
}

type QRCodeSettings struct {
Enabled bool
IncludeDNS bool
IncludeMTU bool
Enabled bool
IncludeDNS bool
IncludeMTU bool
}
27 changes: 27 additions & 0 deletions store/jsondb/jsondb.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path"
"strconv"
"time"

"github.com/sdomino/scribble"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -314,6 +329,17 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti
func (o *JsonDB) SaveClient(client model.Client) error {
clientPath := path.Join(path.Join(o.dbPath, "clients"), client.ID+".json")
output := o.conn.Write("clients", client.ID, client)
if output == nil {
if client.Enabled && len(client.TgUserid) > 0 {
if userid, err := strconv.ParseInt(client.TgUserid, 10, 64); err == nil {
util.UpdateTgToClientID(userid, client.ID)
}
} else {
util.RemoveTgToClientID(client.ID)
}
} else {
util.RemoveTgToClientID(client.ID)
}
err := util.ManagePerms(clientPath)
if err != nil {
return err
Expand All @@ -322,6 +348,7 @@ func (o *JsonDB) SaveClient(client model.Client) error {
}

func (o *JsonDB) DeleteClient(clientID string) error {
util.RemoveTgToClientID(clientID)
return o.conn.Delete("clients", clientID)
}

Expand Down
Loading

0 comments on commit 41bf0bc

Please sign in to comment.