- Use updated version of upstream go-xmpp + fixes on client-side - Still waiting for https://github.com/xmppo/go-xmpp/pull/215 - Introduce golangci-lint v2 + fixes
602 lines
15 KiB
Go
602 lines
15 KiB
Go
package jabber
|
|
|
|
import (
|
|
"buny-jabber-bot/internal/log"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/xmppo/go-xmpp"
|
|
)
|
|
|
|
// BunyPresense производит проверку по бело-чёрным спискам. Если presence пришёл от злодея (из чёрного списка), то
|
|
// отправляет его в бан.
|
|
func (j *Jabber) BunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo
|
|
var err error
|
|
|
|
// Если у presence-а есть JID и presence из одной из комнат, в которой мы есть и если его домен в чёрном
|
|
// списке, заносим пидора в список outcast-ов.
|
|
if v.JID != "" {
|
|
// На всякий случай: себя никогда не баним, явным образом.
|
|
if v.JID == j.Talk.JID() {
|
|
return err
|
|
}
|
|
|
|
room := strings.SplitN(v.From, "/", 2)[0]
|
|
|
|
if room == "" {
|
|
log.Infof("We got empty room field in presence event, which kinda strange: %s", spew.Sdump(v))
|
|
|
|
return err
|
|
}
|
|
|
|
goodJid := strings.SplitN(v.JID, "/", 2)[0]
|
|
|
|
// Обрабатываем правила белого списка.
|
|
for _, good := range j.WhiteList.Whitelist {
|
|
// Глобальный белый список.
|
|
if good.RoomName == "" {
|
|
if slices.Contains(good.Jid, goodJid) {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Белый список для конкретных каналов.
|
|
if good.RoomName == room {
|
|
if slices.Contains(good.Jid, goodJid) {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
id, err := j.QuerySoftwareVersion(v.From)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to query user software and version: jid=%s id=%s, err=%w",
|
|
v.JID,
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
|
|
return err
|
|
}
|
|
|
|
evilJid := strings.SplitN(v.JID, "/", 2)[0]
|
|
evilNick := ""
|
|
evilNicks := strings.SplitN(v.From, "/", 2)
|
|
|
|
// Давайте-ка похэндлим нонсенс - когда у нас в строке нету разделителя.
|
|
if len(evilNicks) > 1 {
|
|
evilNick = evilNicks[1]
|
|
}
|
|
|
|
// Обрабатываем правила чёрного списка.
|
|
for _, cRoom := range j.RoomsConnected {
|
|
if cRoom == room {
|
|
for _, bEntry := range j.BlackList.Blacklist {
|
|
// Обработаем правила глобального чёрного списка.
|
|
if bEntry.RoomName == "" {
|
|
for _, jidRegexp := range bEntry.JidRe {
|
|
if jidRegexp == "" {
|
|
continue
|
|
}
|
|
|
|
re, err := regexp.Compile(jidRegexp)
|
|
|
|
if err != nil {
|
|
log.Errorf("Incorrect regexp in global blacklist: %s, skipping", jidRegexp)
|
|
|
|
continue
|
|
}
|
|
|
|
log.Debugf("Checking jid %s vs global blacklist regex %s", v.JID, jidRegexp)
|
|
|
|
if re.MatchString(v.JID) {
|
|
log.Warnf(
|
|
"Hammer falls on %s (%s): jid matches with global blacklist entry: %s",
|
|
v.From,
|
|
evilJid,
|
|
jidRegexp,
|
|
)
|
|
|
|
id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, nickRegexp := range bEntry.NickRe {
|
|
if nickRegexp == "" {
|
|
continue
|
|
}
|
|
|
|
re, err := regexp.Compile(nickRegexp)
|
|
|
|
if err != nil {
|
|
log.Errorf("Incorrect regexp in global blacklist: %s, skipping", nickRegexp)
|
|
|
|
continue
|
|
}
|
|
|
|
log.Debugf("Checking nick %s vs global blacklist regex %s", evilNick, nickRegexp)
|
|
|
|
if re.MatchString(evilNick) {
|
|
log.Warnf(
|
|
"Hammer falls on %s (%s): nick matches with global blacklist entry: %s",
|
|
v.From,
|
|
evilJid,
|
|
nickRegexp,
|
|
)
|
|
|
|
// Баним именно jid.
|
|
id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
// Обработаем правила конкретного канала, комнаты конференции.
|
|
if bEntry.RoomName == room {
|
|
for _, jidRegexp := range bEntry.JidRe {
|
|
if jidRegexp == "" {
|
|
continue
|
|
}
|
|
|
|
re, err := regexp.Compile(jidRegexp)
|
|
|
|
if err != nil {
|
|
log.Errorf("Incorrect regexp in room %s blacklist: %s, skipping", room, jidRegexp)
|
|
|
|
continue
|
|
}
|
|
|
|
log.Debugf("Checking jid %s vs room %s blacklist regex %s", v.JID, room, jidRegexp)
|
|
|
|
if re.MatchString(v.JID) {
|
|
log.Warnf(
|
|
"Hammer falls on %s (%s): jid matches with room blacklist entry: %s",
|
|
v.From,
|
|
evilJid,
|
|
jidRegexp,
|
|
)
|
|
|
|
id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
if bEntry.RoomName == room {
|
|
for _, nickRegexp := range bEntry.NickRe {
|
|
if nickRegexp == "" {
|
|
continue
|
|
}
|
|
|
|
re, err := regexp.Compile(nickRegexp)
|
|
|
|
if err != nil {
|
|
log.Errorf("Incorrect regexp in room %s blacklist: %s, skipping", room, nickRegexp)
|
|
|
|
continue
|
|
}
|
|
|
|
log.Debugf("Checking nick %s vs room %s blacklist regex %s", evilJid, room, nickRegexp)
|
|
|
|
if re.MatchString(evilNick) {
|
|
log.Warnf(
|
|
"Hammer falls on %s (%s): nick matches with room blacklist entry: %s",
|
|
v.From,
|
|
evilJid,
|
|
nickRegexp,
|
|
)
|
|
|
|
// Баним именно jid.
|
|
id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// BunyChat производит проверку сообщений участников чата по списку забаненных фраз и в случае нахождения запрещённого
|
|
// шаблона банит участника чата.
|
|
func (j *Jabber) BunyChat(v xmpp.Chat) error {
|
|
var (
|
|
room = (strings.SplitN(v.Remote, "/", 2))[0]
|
|
err error
|
|
)
|
|
|
|
// Действовать мы можем только в рамках тех комнат, где явно присуствуем.
|
|
for _, cRoom := range j.RoomsConnected {
|
|
if cRoom == room {
|
|
// Перебирём правила чёрных списков.
|
|
for _, bEntry := range j.BlackList.Blacklist {
|
|
// Обработаем правила глобального чёрного списка.
|
|
if bEntry.RoomName == "" {
|
|
for _, phraseRegexp := range bEntry.PhraseRe {
|
|
if phraseRegexp == "" {
|
|
continue
|
|
}
|
|
|
|
re, err := regexp.Compile(phraseRegexp)
|
|
|
|
if err != nil {
|
|
log.Errorf("Incorrect regexp in room %s blacklist: %s, skipping", room, phraseRegexp)
|
|
|
|
continue
|
|
}
|
|
|
|
log.Debugf("Checking phrase %s vs room %s blacklist regex %s", v.Text, room, phraseRegexp)
|
|
|
|
if re.MatchString(v.Text) {
|
|
realJID := j.GetRealJIDfromNick(v.Remote)
|
|
|
|
log.Warnf(
|
|
"Hammer falls on %s (%s): phrase matches with global blacklist entry: %s vs %s",
|
|
v.Remote,
|
|
realJID,
|
|
v.Text,
|
|
phraseRegexp,
|
|
)
|
|
|
|
if id, err := j.Squash(room, realJID, bEntry.ReasonEnable, v.Type); err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Обработаем правила чёрного списка для конкретной комнаты.
|
|
if bEntry.RoomName == room {
|
|
for _, phraseRegexp := range bEntry.PhraseRe {
|
|
if phraseRegexp == "" {
|
|
continue
|
|
}
|
|
|
|
re, err := regexp.Compile(phraseRegexp)
|
|
|
|
if err != nil {
|
|
log.Errorf("Incorrect regexp in room %s blacklist: %s, skipping", room, phraseRegexp)
|
|
|
|
continue
|
|
}
|
|
|
|
log.Debugf("Checking phrase %s vs room %s blacklist regex %s", v.Text, room, phraseRegexp)
|
|
|
|
if re.MatchString(v.Text) {
|
|
realJID := j.GetRealJIDfromNick(v.Remote)
|
|
|
|
log.Warnf(
|
|
"Hammer falls on %s (%s): phrase matches with room blacklist entry: %s vs %s",
|
|
v.Remote,
|
|
realJID,
|
|
v.Text,
|
|
phraseRegexp,
|
|
)
|
|
|
|
if id, err := j.Squash(room, realJID, bEntry.ReasonEnable, v.Type); err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Если включено, проверяем фразу на КАПС.
|
|
for _, channel := range j.C.Jabber.Channels {
|
|
if channel.Name == room && channel.AllCaps.Enabled {
|
|
// Нормализуем строку и вырежем из неё пробелы.
|
|
normPhrase := strings.ReplaceAll(nString(v.Text), " ", "")
|
|
|
|
// Проверяем согласно тому, что длина фразы более чем сколько-то символов.
|
|
if len(normPhrase) >= channel.AllCaps.MinLength {
|
|
normPhraseUpper := strings.ReplaceAll(nStringUpper(v.Text), " ", "")
|
|
|
|
if normPhrase == normPhraseUpper {
|
|
realJID := j.GetRealJIDfromNick(v.Remote)
|
|
id, err := j.Squash(room, realJID, false, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// BunySoftwareVersion производит проверку по чёрному списку версий и названий клинтского ПО.
|
|
func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) error {
|
|
var err error
|
|
|
|
if v.From != "" {
|
|
// На всякий случай: себя никогда не баним, явным образом.
|
|
if v.From == j.Talk.JID() {
|
|
return err
|
|
}
|
|
|
|
room := strings.SplitN(v.From, "/", 2)[0]
|
|
|
|
var (
|
|
presenceJSONStrings []string
|
|
)
|
|
|
|
if presenceJSONInterface, present := j.RoomPresences.Get(room); present {
|
|
presenceJSONStrings = InterfaceToStringSlice(presenceJSONInterface)
|
|
}
|
|
|
|
log.Debugf("Looking up %s in presence db", v.From)
|
|
|
|
for _, presenceJSONString := range presenceJSONStrings {
|
|
var p xmpp.Presence
|
|
|
|
_ = json.Unmarshal([]byte(presenceJSONString), &p)
|
|
|
|
if v.From == p.From {
|
|
|
|
log.Infof("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)
|
|
|
|
continue
|
|
}
|
|
|
|
// На всякий случай.
|
|
if p.Role == "moderator" {
|
|
log.Infof("Skipping useragent blacklist check for %s because his role is %s", v.From, p.Role)
|
|
|
|
continue
|
|
}
|
|
|
|
// Lookup in global blacklist.
|
|
for _, cRoom := range j.RoomsConnected {
|
|
if cRoom == room {
|
|
for _, bEntry := range j.BlackList.Blacklist {
|
|
// Обработаем правила глобального чёрного списка.
|
|
if bEntry.RoomName == "" {
|
|
for _, useragent := range bEntry.UserAgent {
|
|
switch {
|
|
case useragent.Name != "" && useragent.Version != "" && useragent.Os != "":
|
|
if ver.Name == useragent.Name && ver.Version == useragent.Version && ver.Os == useragent.Os {
|
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
case useragent.Name != "" && useragent.Version != "":
|
|
if ver.Name == useragent.Name && ver.Version == useragent.Version {
|
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
case useragent.Name != "":
|
|
if ver.Name == useragent.Name {
|
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
case useragent.Version != "":
|
|
if ver.Version == useragent.Version {
|
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if bEntry.RoomName == room {
|
|
for _, useragent := range bEntry.UserAgent {
|
|
switch {
|
|
case useragent.Name != "" && useragent.Version != "" && useragent.Os != "":
|
|
if ver.Name == useragent.Name && ver.Version == useragent.Version && ver.Os == useragent.Os {
|
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
case useragent.Name != "" && useragent.Version != "":
|
|
if ver.Name == useragent.Name && ver.Version == useragent.Version {
|
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
case useragent.Name != "":
|
|
if ver.Name == useragent.Name {
|
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
case useragent.Version != "":
|
|
if ver.Version == useragent.Version {
|
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf(
|
|
"unable to ban user: id=%s, err=%w",
|
|
id,
|
|
err,
|
|
)
|
|
|
|
j.GTomb.Kill(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|