diff --git a/web/global/hashStorage.go b/web/global/hashStorage.go new file mode 100644 index 00000000..099a54ed --- /dev/null +++ b/web/global/hashStorage.go @@ -0,0 +1,91 @@ +package global + +import ( + "crypto/md5" + "encoding/hex" + "sync" + "time" +) + +type HashEntry struct { + Hash string + Value string + Timestamp time.Time +} + +type HashStorage struct { + sync.RWMutex + Data map[string]HashEntry + Expiration time.Duration + ForceSave bool +} + +func NewHashStorage(expiration time.Duration, forceSave bool) *HashStorage { + return &HashStorage{ + Data: make(map[string]HashEntry), + Expiration: expiration, + ForceSave: forceSave, + } +} + +func (h *HashStorage) AddHash(query string) string { + if h.ForceSave { + return h.saveValue(query) + } + + // we only need to hash for more than 64 chars by default + if len(query) <= 64 { + return query + } + + return h.saveValue(query) +} + +func (h *HashStorage) saveValue(query string) string { + h.Lock() + defer h.Unlock() + + md5Hash := md5.Sum([]byte(query)) + md5HashString := hex.EncodeToString(md5Hash[:]) + + entry := HashEntry{ + Hash: md5HashString, + Value: query, + Timestamp: time.Now(), + } + + h.Data[md5HashString] = entry + + return md5HashString +} + +func (h *HashStorage) GetValue(hash string) string { + h.RLock() + defer h.RUnlock() + + entry, exists := h.Data[hash] + if !exists { + return hash + } + return entry.Value +} + +func (h *HashStorage) RemoveExpiredHashes() { + h.Lock() + defer h.Unlock() + + now := time.Now() + + for hash, entry := range h.Data { + if now.Sub(entry.Timestamp) > h.Expiration { + delete(h.Data, hash) + } + } +} + +func (h *HashStorage) Reset() { + h.Lock() + defer h.Unlock() + + h.Data = make(map[string]HashEntry) +} diff --git a/web/job/check_hash_storage.go b/web/job/check_hash_storage.go new file mode 100644 index 00000000..468aa2e0 --- /dev/null +++ b/web/job/check_hash_storage.go @@ -0,0 +1,19 @@ +package job + +import ( + "x-ui/web/service" +) + +type CheckHashStorageJob struct { + tgbotService service.Tgbot +} + +func NewCheckHashStorageJob() *CheckHashStorageJob { + return new(CheckHashStorageJob) +} + +// Here Run is an interface method of the Job interface +func (j *CheckHashStorageJob) Run() { + // Remove expired hashes from storage + j.tgbotService.GetHashStorage().RemoveExpiredHashes() +} diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 3dcb6df0..8777cbbd 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -12,6 +12,7 @@ import ( "x-ui/database/model" "x-ui/logger" "x-ui/util/common" + "x-ui/web/global" "x-ui/web/locale" "x-ui/xray" @@ -39,6 +40,7 @@ type Tgbot struct { serverService ServerService xrayService XrayService lastStatus *Status + hashStorage *global.HashStorage } func (t *Tgbot) NewTgbot() *Tgbot { @@ -49,12 +51,19 @@ func (t *Tgbot) BotI18n(name string, params ...string) string { return locale.I18n(locale.Bot, name, params...) } +func (t *Tgbot) GetHashStorage() *global.HashStorage { + return t.hashStorage +} + func (t *Tgbot) Start(i18nFS embed.FS) error { err := locale.InitLocalizer(i18nFS, &t.settingService) if err != nil { return err } + // init hash storage + t.hashStorage = global.NewHashStorage(5*time.Minute, false) + tgBottoken, err := t.settingService.GetTgBotToken() if err != nil || tgBottoken == "" { logger.Warning("Get TgBotToken failed:", err) @@ -190,7 +199,10 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool chatId := callbackQuery.Message.Chat.ID if isAdmin { - dataArray := strings.Split(callbackQuery.Data, " ") + // get query from hash storage (if the query was <= 64 chars hash storage dont save the hash and return data itself) + decodedQuery := t.hashStorage.GetValue(callbackQuery.Data) + dataArray := strings.Split(decodedQuery, " ") + if len(dataArray) >= 2 && len(dataArray[1]) > 0 { email := dataArray[1] switch dataArray[0] { @@ -215,10 +227,10 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool case "reset_traffic": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Cancel Reset").WithCallbackData("client_cancel "+email), + tu.InlineKeyboardButton("❌ Cancel Reset").WithCallbackData(t.hashStorage.AddHash("client_cancel "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("✅ Confirm Reset Traffic?").WithCallbackData("reset_traffic_c "+email), + tu.InlineKeyboardButton("✅ Confirm Reset Traffic?").WithCallbackData(t.hashStorage.AddHash("reset_traffic_c "+email)), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) @@ -234,26 +246,26 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool case "reset_exp": var inlineKeyboard = tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Cancel Reset").WithCallbackData("client_cancel "+email), + tu.InlineKeyboardButton("❌ Cancel Reset").WithCallbackData(t.hashStorage.AddHash("client_cancel "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("♾ Unlimited").WithCallbackData("reset_exp_c "+email+" 0"), + tu.InlineKeyboardButton("♾ Unlimited").WithCallbackData(t.hashStorage.AddHash("reset_exp_c "+email+" 0")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("1 Month").WithCallbackData("reset_exp_c "+email+" 30"), - tu.InlineKeyboardButton("2 Months").WithCallbackData("reset_exp_c "+email+" 60"), + tu.InlineKeyboardButton("1 Month").WithCallbackData(t.hashStorage.AddHash("reset_exp_c "+email+" 30")), + tu.InlineKeyboardButton("2 Months").WithCallbackData(t.hashStorage.AddHash("reset_exp_c "+email+" 60")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("3 Months").WithCallbackData("reset_exp_c "+email+" 90"), - tu.InlineKeyboardButton("6 Months").WithCallbackData("reset_exp_c "+email+" 180"), + tu.InlineKeyboardButton("3 Months").WithCallbackData(t.hashStorage.AddHash("reset_exp_c "+email+" 90")), + tu.InlineKeyboardButton("6 Months").WithCallbackData(t.hashStorage.AddHash("reset_exp_c "+email+" 180")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("9 Months").WithCallbackData("reset_exp_c "+email+" 270"), - tu.InlineKeyboardButton("12 Months").WithCallbackData("reset_exp_c "+email+" 360"), + tu.InlineKeyboardButton("9 Months").WithCallbackData(t.hashStorage.AddHash("reset_exp_c "+email+" 270")), + tu.InlineKeyboardButton("12 Months").WithCallbackData(t.hashStorage.AddHash("reset_exp_c "+email+" 360")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("10 Days").WithCallbackData("reset_exp_c "+email+" 10"), - tu.InlineKeyboardButton("20 Days").WithCallbackData("reset_exp_c "+email+" 20"), + tu.InlineKeyboardButton("10 Days").WithCallbackData(t.hashStorage.AddHash("reset_exp_c "+email+" 10")), + tu.InlineKeyboardButton("20 Days").WithCallbackData(t.hashStorage.AddHash("reset_exp_c "+email+" 20")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) @@ -279,28 +291,28 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool case "ip_limit": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Cancel IP Limit").WithCallbackData("client_cancel "+email), + tu.InlineKeyboardButton("❌ Cancel IP Limit").WithCallbackData(t.hashStorage.AddHash("client_cancel "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("♾ Unlimited").WithCallbackData("ip_limit_c "+email+" 0"), + tu.InlineKeyboardButton("♾ Unlimited").WithCallbackData(t.hashStorage.AddHash("ip_limit_c "+email+" 0")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("1").WithCallbackData("ip_limit_c "+email+" 1"), - tu.InlineKeyboardButton("2").WithCallbackData("ip_limit_c "+email+" 2"), + tu.InlineKeyboardButton("1").WithCallbackData(t.hashStorage.AddHash("ip_limit_c "+email+" 1")), + tu.InlineKeyboardButton("2").WithCallbackData(t.hashStorage.AddHash("ip_limit_c "+email+" 2")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("3").WithCallbackData("ip_limit_c "+email+" 3"), - tu.InlineKeyboardButton("4").WithCallbackData("ip_limit_c "+email+" 4"), + tu.InlineKeyboardButton("3").WithCallbackData(t.hashStorage.AddHash("ip_limit_c "+email+" 3")), + tu.InlineKeyboardButton("4").WithCallbackData(t.hashStorage.AddHash("ip_limit_c "+email+" 4")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("5").WithCallbackData("ip_limit_c "+email+" 5"), - tu.InlineKeyboardButton("6").WithCallbackData("ip_limit_c "+email+" 6"), - tu.InlineKeyboardButton("7").WithCallbackData("ip_limit_c "+email+" 7"), + tu.InlineKeyboardButton("5").WithCallbackData(t.hashStorage.AddHash("ip_limit_c "+email+" 5")), + tu.InlineKeyboardButton("6").WithCallbackData(t.hashStorage.AddHash("ip_limit_c "+email+" 6")), + tu.InlineKeyboardButton("7").WithCallbackData(t.hashStorage.AddHash("ip_limit_c "+email+" 7")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("8").WithCallbackData("ip_limit_c "+email+" 8"), - tu.InlineKeyboardButton("9").WithCallbackData("ip_limit_c "+email+" 9"), - tu.InlineKeyboardButton("10").WithCallbackData("ip_limit_c "+email+" 10"), + tu.InlineKeyboardButton("8").WithCallbackData(t.hashStorage.AddHash("ip_limit_c "+email+" 8")), + tu.InlineKeyboardButton("9").WithCallbackData(t.hashStorage.AddHash("ip_limit_c "+email+" 9")), + tu.InlineKeyboardButton("10").WithCallbackData(t.hashStorage.AddHash("ip_limit_c "+email+" 10")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) @@ -322,10 +334,10 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool case "clear_ips": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Cancel").WithCallbackData("ips_cancel "+email), + tu.InlineKeyboardButton("❌ Cancel").WithCallbackData(t.hashStorage.AddHash("ips_cancel "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("✅ Confirm Clear IPs?").WithCallbackData("clear_ips_c "+email), + tu.InlineKeyboardButton("✅ Confirm Clear IPs?").WithCallbackData(t.hashStorage.AddHash("clear_ips_c "+email)), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) @@ -346,10 +358,10 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool case "tgid_remove": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Cancel").WithCallbackData("tgid_cancel "+email), + tu.InlineKeyboardButton("❌ Cancel").WithCallbackData(t.hashStorage.AddHash("tgid_cancel "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("✅ Confirm Remove Telegram User?").WithCallbackData("tgid_remove_c "+email), + tu.InlineKeyboardButton("✅ Confirm Remove Telegram User?").WithCallbackData(t.hashStorage.AddHash("tgid_remove_c "+email)), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) @@ -418,21 +430,21 @@ func checkAdmin(tgId int64) bool { func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) { numericKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("Server Usage").WithCallbackData("get_usage"), - tu.InlineKeyboardButton("Get DB Backup").WithCallbackData("get_backup"), + tu.InlineKeyboardButton("Server Usage").WithCallbackData(t.hashStorage.AddHash("get_usage")), + tu.InlineKeyboardButton("Get DB Backup").WithCallbackData(t.hashStorage.AddHash("get_backup")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("Get Inbounds").WithCallbackData("inbounds"), - tu.InlineKeyboardButton("Deplete soon").WithCallbackData("deplete_soon"), + tu.InlineKeyboardButton("Get Inbounds").WithCallbackData(t.hashStorage.AddHash("inbounds")), + tu.InlineKeyboardButton("Deplete soon").WithCallbackData(t.hashStorage.AddHash("deplete_soon")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("Commands").WithCallbackData("commands"), + tu.InlineKeyboardButton("Commands").WithCallbackData(t.hashStorage.AddHash("commands")), ), ) numericKeyboardClient := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("Get Usage").WithCallbackData("client_traffic"), - tu.InlineKeyboardButton("Commands").WithCallbackData("client_commands"), + tu.InlineKeyboardButton("Get Usage").WithCallbackData(t.hashStorage.AddHash("client_traffic")), + tu.InlineKeyboardButton("Commands").WithCallbackData(t.hashStorage.AddHash("client_commands")), ), ) var ReplyMarkup telego.ReplyMarkup @@ -678,10 +690,10 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) { output := fmt.Sprintf("📧 Email: %s\r\n🔢 IPs: \r\n%s\r\n", email, ips) inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("🔄 Refresh").WithCallbackData("ips_refresh "+email), + tu.InlineKeyboardButton("🔄 Refresh").WithCallbackData(t.hashStorage.AddHash("ips_refresh "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Clear IPs").WithCallbackData("clear_ips "+email), + tu.InlineKeyboardButton("❌ Clear IPs").WithCallbackData(t.hashStorage.AddHash("clear_ips "+email)), ), ) if len(messageID) > 0 { @@ -712,10 +724,10 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ... output := fmt.Sprintf("📧 Email: %s\r\n👤 Telegram User: %s\r\n", email, tgId) inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("🔄 Refresh").WithCallbackData("tgid_refresh "+email), + tu.InlineKeyboardButton("🔄 Refresh").WithCallbackData(t.hashStorage.AddHash("tgid_refresh "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Remove Telegram User").WithCallbackData("tgid_remove "+email), + tu.InlineKeyboardButton("❌ Remove Telegram User").WithCallbackData(t.hashStorage.AddHash("tgid_remove "+email)), ), ) @@ -775,23 +787,23 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) { inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("🔄 Refresh").WithCallbackData("client_refresh "+email), + tu.InlineKeyboardButton("🔄 Refresh").WithCallbackData(t.hashStorage.AddHash("client_refresh "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("📈 Reset Traffic").WithCallbackData("reset_traffic "+email), + tu.InlineKeyboardButton("📈 Reset Traffic").WithCallbackData(t.hashStorage.AddHash("reset_traffic "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("📅 Reset Expire Days").WithCallbackData("reset_exp "+email), + tu.InlineKeyboardButton("📅 Reset Expire Days").WithCallbackData(t.hashStorage.AddHash("reset_exp "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("🔢 IP Log").WithCallbackData("ip_log "+email), - tu.InlineKeyboardButton("🔢 IP Limit").WithCallbackData("ip_limit "+email), + tu.InlineKeyboardButton("🔢 IP Log").WithCallbackData(t.hashStorage.AddHash("ip_log "+email)), + tu.InlineKeyboardButton("🔢 IP Limit").WithCallbackData(t.hashStorage.AddHash("ip_limit "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("👤 Set Telegram User").WithCallbackData("tg_user "+email), + tu.InlineKeyboardButton("👤 Set Telegram User").WithCallbackData(t.hashStorage.AddHash("tg_user "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("🔘 Enable / Disable").WithCallbackData("toggle_enable "+email), + tu.InlineKeyboardButton("🔘 Enable / Disable").WithCallbackData(t.hashStorage.AddHash("toggle_enable "+email)), ), ) diff --git a/web/web.go b/web/web.go index 908fefc5..b15731fc 100644 --- a/web/web.go +++ b/web/web.go @@ -283,6 +283,9 @@ func (s *Server) startTask() { return } + // check for Telegram bot callback query hash storage reset + s.cron.AddJob("@every 2m", job.NewCheckHashStorageJob()) + // Check CPU load and alarm to TgBot if threshold passes cpuThreshold, err := s.settingService.GetTgCpu() if (err == nil) && (cpuThreshold > 0) {