From de3db48d478c679f3fc2674c894f28d306521aca Mon Sep 17 00:00:00 2001 From: eleksir Date: Sun, 21 Dec 2025 19:07:48 +0300 Subject: [PATCH] Fix filling presence db, fix ban by useragent --- data/sample_blacklist.json | 2 +- data/sample_config.json | 4 +- internal/jabber/black-whilte_lists.go | 26 +++- internal/jabber/buny.go | 77 +++++++---- internal/jabber/event_parser.go | 177 ++++++++++++++++++++------ internal/jabber/types.go | 37 +++--- internal/jabber/util.go | 53 +++++--- 7 files changed, 273 insertions(+), 103 deletions(-) diff --git a/data/sample_blacklist.json b/data/sample_blacklist.json index 2d6fbcc..9cf5b48 100644 --- a/data/sample_blacklist.json +++ b/data/sample_blacklist.json @@ -29,7 +29,7 @@ "user_agent": [ { - "software": "BadUserAgent", + "name": "BadUserAgent", "version": "0.666.0", "os": "DeathStarOS" } diff --git a/data/sample_config.json b/data/sample_config.json index b9260ec..f43372c 100644 --- a/data/sample_config.json +++ b/data/sample_config.json @@ -204,6 +204,8 @@ # логгирует то, что пишется "в провод" "lib": false, # Дебаг периодических пингов. Мало полезная вещь, разве что для выявления сетевых аномалий. - "ping": false + "ping": false, + # Дебаг работы presence db. + "presence_db": false } } diff --git a/internal/jabber/black-whilte_lists.go b/internal/jabber/black-whilte_lists.go index 88f93a4..e545cbd 100644 --- a/internal/jabber/black-whilte_lists.go +++ b/internal/jabber/black-whilte_lists.go @@ -7,7 +7,9 @@ import ( "fmt" "os" "path/filepath" + "strings" + "github.com/davecgh/go-spew/spew" "github.com/hjson/hjson-go" ) @@ -123,13 +125,13 @@ func (j *Jabber) ReadBlacklist() error { return err } - whitelistJSONPath := filepath.Dir(executablePath) + "/data/blacklist.json" + blacklistJSONPath := filepath.Dir(executablePath) + "/data/blacklist.json" locations := []string{ - "~/.bunyPresense-jabber-bot-blacklist.json", - "~/bunyPresense-jabber-bot-blacklist.json", - "/etc/bunyPresense-jabber-bot-blacklist.json", - whitelistJSONPath, + "~/.buny-jabber-bot-blacklist.json", + "~/buny-jabber-bot-blacklist.json", + "/etc/buny-jabber-bot-blacklist.json", + blacklistJSONPath, } for _, location := range locations { @@ -188,7 +190,19 @@ func (j *Jabber) ReadBlacklist() error { continue } - j.BlackList = sampleBlacklist + for _, entry := range sampleBlacklist.Blacklist { + for str := range strings.SplitSeq(spew.Sdump(entry), "\n") { + log.Debug(str) + } + } + + // Вообще говоря, нас уверяют, что простого "=" хватает, но на практике оказывается, что вложенные стуктуры + // могут и не скопироваться. + tmpjson, _ := json.Marshal(sampleBlacklist) //nolint: errchkjson // Да блять, сколько можно + // доёбываться, эти данные 100500 раз перекодировались, там ошибка возможна только если есть ошибка в + // encoding/json, но в этом случае мы в любом случае ничего поделать не можем, вот всяком случае в рантайме. + _ = json.Unmarshal(tmpjson, &j.BlackList) + blacklistLoaded = true log.Infof("Using %s as blacklist file", location) diff --git a/internal/jabber/buny.go b/internal/jabber/buny.go index 68ae267..eb59520 100644 --- a/internal/jabber/buny.go +++ b/internal/jabber/buny.go @@ -403,28 +403,42 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err if v.From != "" { // На всякий случай: себя никогда не баним, явным образом. if v.From == j.Talk.JID() { + log.Debugf("Skipping useragent blacklist check for myself") + return err } room := strings.SplitN(v.From, "/", 2)[0] - var ( - presenceJSONStrings []string - ) - if presenceJSONInterface, present := j.RoomPresences.Get(room); present { - presenceJSONStrings = InterfaceToStringSlice(presenceJSONInterface) - } + presenceJSON, ok := presenceJSONInterface.([]byte) - log.Debugf("Looking up %s in presence db", v.From) + if !ok { + log.Errorf("Looks like we have corrupted entry in presence db") - for _, presenceJSONString := range presenceJSONStrings { - var p xmpp.Presence + return nil + } - _ = json.Unmarshal([]byte(presenceJSONString), &p) + var presences []xmpp.Presence - if v.From == p.From { - log.Infof("Found jid of %s in presence db: %s", v.From, p.JID) + if err := json.Unmarshal(presenceJSON, &presences); err != nil { + log.Errorf("Looks like we have corrupted entry in presence db") + + return nil //nolint: nilerr // Это не критическая ошибка, её не надо возвращать. + } + + for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука. + // Если находим, что у нас уже есть клиент с таким же From, то есть полным nick-ом (для groupchat) + // пропускаем его. + if p.From != v.From { + log.Debugf("Skip check for %s because it is not %s", p.From, v.From) + + continue + } + + if j.C.Debug.PresenceDB { + log.Debugf("Found jid of %s in presence db: %s", v.From, p.JID) + } if p.Affiliation == "admin" || p.Affiliation == "owner" { log.Infof("Skipping useragent blacklist check for %s because his affiliation is %s", v.From, p.Affiliation) @@ -442,7 +456,7 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err log.Debug("Useragent is not owner or moderator, proceeding with check...") // Lookup in global blacklist. - log.Debug("Find our room in connected room list...") + log.Debug("Searching our room in connected room list...") for _, cRoom := range j.RoomsConnected { if cRoom == room { @@ -452,12 +466,15 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err // Обработаем правила глобального чёрного списка. if bEntry.RoomName == "" { log.Debugf("Check in global blacklist") + log.Debugf("We have useragent struct with Name=%s, Version=%s, OS=%s", ver.Name, ver.Version, ver.Os) for _, useragent := range bEntry.UserAgent { + log.Debugf("We have useragent blacklist entry Name=%s, Version=%s, OS=%s", useragent.Name, useragent.Version, useragent.Os) + switch { case useragent.Name != "" && useragent.Version != "" && useragent.Os != "": if ver.Name == useragent.Name && ver.Version == useragent.Version && ver.Os == useragent.Os { - log.Debug("Found all 3 components in black list, it is ban") + log.Warnf("Hammer falls on %s (%s): Software Name, Version and OS match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) @@ -474,9 +491,9 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err return err } - case useragent.Name != "" && useragent.Version != "": + case useragent.Name != "" && useragent.Version != "" && useragent.Os == "": if ver.Name == useragent.Name && ver.Version == useragent.Version { - log.Debug("Found sw name and version in blacklist, it is ban") + log.Warnf("Hammer falls on %s (%s): Software Name and Version match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) @@ -493,9 +510,9 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err return err } - case useragent.Name != "": + case useragent.Name != "" && useragent.Version == "" && useragent.Os == "": if ver.Name == useragent.Name { - log.Debug("Found sw name in blacklist, it is ban") + log.Warnf("Hammer falls on %s (%s): Software Name match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) @@ -512,9 +529,9 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err return err } - case useragent.Version != "": + case useragent.Name == "" && useragent.Version != "" && useragent.Os == "": if ver.Version == useragent.Version { - log.Debug("Found sw version in blacklist, it is ban") + log.Warnf("Hammer falls on %s (%s): Software Version match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) @@ -532,16 +549,20 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err } } } + + log.Debug("No matches in global software versions blacklist") } if bEntry.RoomName == room { log.Debug("Found blacklist entries for room") for _, useragent := range bEntry.UserAgent { + log.Debugf("We have useragent blacklist entry Name=%s, Version=%s, OS=%s", useragent.Name, useragent.Version, useragent.Os) + switch { case useragent.Name != "" && useragent.Version != "" && useragent.Os != "": if ver.Name == useragent.Name && ver.Version == useragent.Version && ver.Os == useragent.Os { - log.Debug("Found all 3 components in black list, it is ban") + log.Warnf("Hammer falls on %s (%s): Software Name, Version and OS match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) @@ -558,9 +579,9 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err return err } - case useragent.Name != "" && useragent.Version != "": + case useragent.Name != "" && useragent.Version != "" && useragent.Os == "": if ver.Name == useragent.Name && ver.Version == useragent.Version { - log.Debug("Found sw name and version in blacklist, it is ban") + log.Warnf("Hammer falls on %s (%s): Software Name and Version match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) @@ -577,9 +598,9 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err return err } - case useragent.Name != "": + case useragent.Name != "" && useragent.Version == "" && useragent.Os == "": if ver.Name == useragent.Name { - log.Debug("Found sw name in blacklist, it is ban") + log.Warnf("Hammer falls on %s (%s): Software Name match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) @@ -596,9 +617,9 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err return err } - case useragent.Version != "": + case useragent.Name == "" && useragent.Version != "" && useragent.Os == "": if ver.Version == useragent.Version { - log.Debug("Found sw version in blacklist, it is ban") + log.Warnf("Hammer falls on %s (%s): Software Version match entry in blacklist", p.From, p.JID) id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type) @@ -616,6 +637,8 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err } } } + + log.Debugf("No matches in software versions blacklist for %s", room) } } } diff --git a/internal/jabber/event_parser.go b/internal/jabber/event_parser.go index 19a0c69..27f897b 100644 --- a/internal/jabber/event_parser.go +++ b/internal/jabber/event_parser.go @@ -632,8 +632,8 @@ func (j *Jabber) ParseEvent(e any) { //nolint:maintidx,gocognit,gocyclo } } else { log.Infof( - "Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affiliation: %s, Role: %s, JID: %s", - v.Type, v.From, v.To, v.Show, v.Status, v.Affiliation, v.Role, v.JID, + "Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affiliation: %s, Role: %s, JID: %s, Priority: %s", + v.Type, v.From, v.To, v.Show, v.Status, v.Affiliation, v.Role, v.JID, v.Priority, ) if !strings.Contains(v.From, "/") { @@ -642,6 +642,12 @@ func (j *Jabber) ParseEvent(e any) { //nolint:maintidx,gocognit,gocyclo return } + if v.JID == "" { + log.Info("Useless presence notification without JID field, skipping") + + return + } + room := strings.SplitN(v.From, "/", 2)[0] nick := strings.SplitN(v.From, "/", 2)[1] @@ -651,68 +657,167 @@ func (j *Jabber) ParseEvent(e any) { //nolint:maintidx,gocognit,gocyclo return } - // Это наш собственный Presence. - if v.Show == "" && v.Status == "" { - if nick == j.GetBotNickFromRoomConfig(room) { - j.RoomsConnected = append(j.RoomsConnected, room) - // На всякий случай дедуплицируем список комнат, к которым мы заджойнились. - sort.Strings(j.RoomsConnected) + // На всякий случай добавим комнату, из которой пришло уведомление в список тех, в которые вхож бот. + if nick == j.GetBotNickFromRoomConfig(room) { + j.RoomsConnected = append(j.RoomsConnected, room) + // На всякий случай дедуплицируем список комнат, к которым мы заджойнились. + sort.Strings(j.RoomsConnected) - j.RoomsConnected = slices.Compact(j.RoomsConnected) - } + j.RoomsConnected = slices.Compact(j.RoomsConnected) } switch v.Role { // Участник ушёл. case "none": + var ( + newPresences = make([]xmpp.Presence, 0) + presenceJSON []byte + ok bool + presences []xmpp.Presence + ) + + // Обновлять базу имеет смысл только если есть хоть какая-то запись о комнате в ней. if presenceJSONInterface, present := j.RoomPresences.Get(room); present { - presenceJSONStrings := InterfaceToStringSlice(presenceJSONInterface) + presenceJSON, ok = presenceJSONInterface.([]byte) - var newPresenceJSONStrings []string + if !ok { + log.Errorf("Looks like we have corrupted entry in presence db") - for _, presenceJSONstring := range presenceJSONStrings { - var p xmpp.Presence + return + } - _ = json.Unmarshal([]byte(presenceJSONstring), &p) + if err := json.Unmarshal(presenceJSON, &presences); err != nil { + log.Errorf("Looks like we have corrupted entry in presence db") + return + } + + for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука. + // Если находим, что у нас уже есть клиент с таким же From, то есть полным nick-ом (для groupchat) + // пропускаем его. if p.From == v.From { + if j.C.Debug.PresenceDB { + log.Debugf("Skipping presence for %s in room %s", v.JID, room) + } + continue } - newPresenceJSONStrings = append(newPresenceJSONStrings, presenceJSONstring) + if j.C.Debug.PresenceDB { + log.Debugf("Appending presence for %s in room %s", v.JID, room) + } + + newPresences = append(newPresences, p) } - j.RoomPresences.Set(room, newPresenceJSONStrings) + presenceJSON, _ = json.Marshal(newPresences) //nolint: errchkjson + + if j.C.Debug.PresenceDB { + log.Debugf("Updating presence db for room %s", room) + } + + j.RoomPresences.Set(room, presenceJSON) + + // Больше нам, похоже с этим уведомлением нечего. + return } - // Участник пришёл. + + // Участник пришёл или изменил свой статус. default: var ( - presenceJSONStrings []string - newPresenceJSONStrings []string - presenceJSONBytes []byte + newPresences = make([]xmpp.Presence, 0) + presenceJSON []byte + ok bool + presences []xmpp.Presence + skip bool ) if presenceJSONInterface, present := j.RoomPresences.Get(room); present { - presenceJSONStrings = InterfaceToStringSlice(presenceJSONInterface) - } + presenceJSON, ok = presenceJSONInterface.([]byte) + if !ok { + log.Errorf("Looks like we have corrupted entry in presence db") - for _, presenceJSONString := range presenceJSONStrings { - var p xmpp.Presence - - _ = json.Unmarshal([]byte(presenceJSONString), &p) - - // Если находим, что у нас уже есть клиент с таким же From, то есть полным nick-ом (для groupchat) - // просто замещаем его. - if p.From == v.From { - continue + return } - newPresenceJSONStrings = append(newPresenceJSONStrings, presenceJSONString) + // Если мы нашли запись о комнате, то её надо обновить. + if err := json.Unmarshal(presenceJSON, &presences); err != nil { + log.Errorf("Looks like we have corrupted entry in presence db") + + return + } + + for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука. + // Если находим, что у нас уже есть клиент с таким же From, то есть полным nick-ом (для groupchat) + // запоминаем его и добавим позднее. + if p.From == v.From { + // Запоминаем участника только если его роль не поменялась на outcast. (То есть если это не + // бан.) + if v.Role == "outcast" { + if j.C.Debug.PresenceDB { + log.Debugf("Let's forget about %s because it is outcast/banned", p.JID) + } + + skip = true + } else if j.C.Debug.PresenceDB { + log.Debugf("Skip adding %s to presences for %s, add as last one", p.JID, room) + } + + continue + } + + if j.C.Debug.PresenceDB { + log.Debugf("Appending presence for %s in room %s", v.JID, room) + } + + newPresences = append(newPresences, p) + } } - presenceJSONBytes, _ = json.Marshal(v) //nolint:errchkjson - newPresenceJSONStrings = append(newPresenceJSONStrings, string(presenceJSONBytes)) - j.RoomPresences.Set(room, newPresenceJSONStrings) + if !skip { + if j.C.Debug.PresenceDB { + log.Debugf("Appending presence for %s in room %s", v.JID, room) + } + + newPresences = append(newPresences, v) + } else if j.C.Debug.PresenceDB { + log.Debugf("Skip Appending outcast presence for %s in room %s", v.JID, room) + } + + presenceJSON, _ = json.Marshal(newPresences) //nolint: errchkjson + + if j.C.Debug.PresenceDB { + log.Debugf("Updating presence db for room %s", room) + } + + j.RoomPresences.Set(room, presenceJSON) + } + + if j.C.Debug.PresenceDB { + log.Debugf("List of presence entries in presence db of %s:", room) + + if presenceJSONInterface, present := j.RoomPresences.Get(room); present { + var presences []xmpp.Presence + + presenceJSON, ok := presenceJSONInterface.([]byte) + + if !ok { + log.Debugf("Looks like we have corrupted entry in presence db") + + return + } + + // Если мы нашли запись о комнате, то её надо обновить. + if err := json.Unmarshal(presenceJSON, &presences); err != nil { + log.Debugf("Looks like we have corrupted entry in presence db") + + return + } + + for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука. + log.Debugf("Found presence for %s from %s", p.JID, room) + } + } } // Проверяем, а не злодей ли зашёл? Сделать это мы можем, только если мы находимся в комнате. diff --git a/internal/jabber/types.go b/internal/jabber/types.go index bbe0b40..e8ee992 100644 --- a/internal/jabber/types.go +++ b/internal/jabber/types.go @@ -15,8 +15,9 @@ type ( Loglevel string `json:"loglevel,omitempty"` Log string `json:"log,omitempty"` Debug struct { - Lib bool `json:"lib,omitempty"` - Ping bool `json:"ping,omitempty"` + Lib bool `json:"lib,omitempty"` + Ping bool `json:"ping,omitempty"` + PresenceDB bool `json:"presence_db,omitempty"` } `json:"debug"` Version string `json:"version,omitempty"` ExeName string `json:"exe_name,omitempty"` @@ -69,7 +70,7 @@ type ( // MyWhiteList прототип структурки с белым списком jid-ов. MyWhiteList struct { Whitelist []struct { - RoomName string `json:"room_name,omitempty"` + RoomName string `json:"room_name"` Jid []string `json:"jid,omitempty"` WipeBans bool `json:"wipe_bans,omitempty"` } `json:"whitelist,omitempty"` @@ -77,18 +78,24 @@ type ( // MyBlackList прототип структурки с чёрным списком jid-ов. MyBlackList struct { - Blacklist []struct { - RoomName string `json:"room_name,omitempty"` - JidRe []string `json:"jid_re,omitempty"` - NickRe []string `json:"nick_re,omitempty"` - PhraseRe []string `json:"phrase_re,omitempty"` - UserAgent []struct { - Name string `json:"name,omitempty"` - Version string `json:"version,omitempty"` - Os string `json:"os,omitempty"` - } `json:"user_agent"` - ReasonEnable bool `json:"reason_enable,omitempty"` - } `json:"blacklist"` + Blacklist []MyBlackListEntry `json:"blacklist"` + } + + // MyBlackListEntry вложенная структура для MyBlackList. + MyBlackListEntry struct { + RoomName string `json:"room_name"` + JidRe []string `json:"jid_re"` + NickRe []string `json:"nick_re"` + PhraseRe []string `json:"phrase_re"` + UserAgent []MyBlackListEntryUserAgent `json:"user_agent"` + ReasonEnable bool `json:"reason_enable,omitempty"` + } + + // MyBlackListEntryUserAgent вложенная структура для MyBlackListEntry. + MyBlackListEntryUserAgent struct { + Name string `json:"name"` + Version string `json:"version"` + Os string `json:"os"` } // Jabber основная структура-объект, содержащая стейты и проч. diff --git a/internal/jabber/util.go b/internal/jabber/util.go index de79a95..d1cafc0 100644 --- a/internal/jabber/util.go +++ b/internal/jabber/util.go @@ -45,23 +45,30 @@ func InterfaceToStringSlice(iface any) []string { // GetRealJIDfromNick достаёт из запомненных presence-ов по даденному nick-у real jid с resource-ом. Nick должен // содержать имя конфы, откуда участник. func (j *Jabber) GetRealJIDfromNick(fullNick string) string { - var p xmpp.Presence + var ( + presenceJSON []byte + ok bool + presences []xmpp.Presence + ) room := (strings.SplitN(fullNick, "/", 2))[0] - // Достанем presence участника. - presenceJSONInterface, present := j.RoomPresences.Get(room) + if presenceJSONInterface, present := j.RoomPresences.Get(room); present { + presenceJSON, ok = presenceJSONInterface.([]byte) + if !ok { + log.Errorf("Looks like we have corrupted entry in presence db") + + return "" + } + } + + if err := json.Unmarshal(presenceJSON, &presences); err != nil { + log.Errorf("Looks like we have corrupted entry in presence db") - // Никого нет дома. - if !present { return "" } - presenceJSONStrings := InterfaceToStringSlice(presenceJSONInterface) - - for _, presepresenceJSONString := range presenceJSONStrings { - _ = json.Unmarshal([]byte(presepresenceJSONString), &p) - + for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука. if p.From == fullNick { return p.JID } @@ -191,17 +198,29 @@ func (j *Jabber) JoinMuc(room string) error { j.GTomb.Go(func() error { return j.RotateStatus(room) }) // Время проверить участников на предмет злобности. - namesInterface, present := j.RoomPresences.Get(room) + presenceJSONInterface, present := j.RoomPresences.Get(room) - // Если room есть в списке presence-ов, то фигачим. room там должен быть, просто обязан. if present { - for _, name := range InterfaceToStringSlice(namesInterface) { - var v xmpp.Presence + presenceJSON, ok := presenceJSONInterface.([]byte) - _ = json.Unmarshal([]byte(name), &v) - log.Infof("Fake presence forged for %s just for on-enter check", v.JID) + if !ok { + log.Errorf("Looks like we have corrupted entry in presence db") + + return nil + } + + var presences []xmpp.Presence + + if err := json.Unmarshal(presenceJSON, &presences); err != nil { + log.Errorf("Looks like we have corrupted entry in presence db") + + return nil //nolint: nilerr // If presence db is corrupted it is a bad thing, but we log it anyway. + } + + for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука. + log.Infof("Fake presence forged for %s just for on-enter check", p.JID) // Оно там внутри всё само обработает, если вдруг возникнет wire error, то зарекконетится. - _ = j.BunyPresense(v) + _ = j.BunyPresense(p) } }