]> git.netwichtig.de Git - user/henk/code/inspircd.git/blob - src/modules/m_repeat.cpp
5a532389a8eef17b7ff35343afd05689589fc522
[user/henk/code/inspircd.git] / src / modules / m_repeat.cpp
1 /*
2  * InspIRCd -- Internet Relay Chat Daemon
3  *
4  *   Copyright (C) 2021 iwalkalone <iwalkalone69@gmail.com>
5  *   Copyright (C) 2019 Robby <robby@chatbelgie.be>
6  *   Copyright (C) 2018-2019 linuxdaemon <linuxdaemon.irc@gmail.com>
7  *   Copyright (C) 2018 Matt Schatz <genius3000@g3k.solutions>
8  *   Copyright (C) 2017-2019 Sadie Powell <sadie@witchery.services>
9  *   Copyright (C) 2015 James Lu <GLolol@overdrivenetworks.com>
10  *   Copyright (C) 2013-2015 Attila Molnar <attilamolnar@hush.com>
11  *   Copyright (C) 2013 Daniel Vassdal <shutter@canternet.org>
12  *
13  * This file is part of InspIRCd.  InspIRCd is free software: you can
14  * redistribute it and/or modify it under the terms of the GNU General Public
15  * License as published by the Free Software Foundation, version 2.
16  *
17  * This program is distributed in the hope that it will be useful, but WITHOUT
18  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
19  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
20  * details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
24  */
25
26
27 #include "inspircd.h"
28 #include "modules/exemption.h"
29
30 class ChannelSettings
31 {
32  public:
33         enum RepeatAction
34         {
35                 ACT_KICK,
36                 ACT_BLOCK,
37                 ACT_BAN
38         };
39
40         RepeatAction Action;
41         unsigned int Backlog;
42         unsigned int Lines;
43         unsigned int Diff;
44         unsigned long Seconds;
45
46         void serialize(std::string& out) const
47         {
48                 if (Action == ACT_BAN)
49                         out.push_back('*');
50                 else if (Action == ACT_BLOCK)
51                         out.push_back('~');
52
53                 out.append(ConvToStr(Lines)).push_back(':');
54                 out.append(ConvToStr(Seconds));
55                 if (Diff)
56                 {
57                         out.push_back(':');
58                         out.append(ConvToStr(Diff));
59                         if (Backlog)
60                         {
61                                 out.push_back(':');
62                                 out.append(ConvToStr(Backlog));
63                         }
64                 }
65         }
66 };
67
68 class RepeatMode : public ParamMode<RepeatMode, SimpleExtItem<ChannelSettings> >
69 {
70  private:
71         struct RepeatItem
72         {
73                 time_t ts;
74                 std::string line;
75                 RepeatItem(time_t TS, const std::string& Line) : ts(TS), line(Line) { }
76         };
77
78         typedef std::deque<RepeatItem> RepeatItemList;
79
80         struct MemberInfo
81         {
82                 RepeatItemList ItemList;
83                 unsigned int Counter;
84                 MemberInfo() : Counter(0) {}
85         };
86
87         struct ModuleSettings
88         {
89                 unsigned int MaxLines;
90                 unsigned int MaxSecs;
91                 unsigned int MaxBacklog;
92                 unsigned int MaxDiff;
93                 unsigned int MaxMessageSize;
94                 std::string KickMessage;
95                 ModuleSettings() : MaxLines(0), MaxSecs(0), MaxBacklog(0), MaxDiff() { }
96         };
97
98         std::vector<unsigned int> mx[2];
99         ModuleSettings ms;
100
101         bool CompareLines(const std::string& message, const std::string& historyline, unsigned int trigger)
102         {
103                 if (message == historyline)
104                         return true;
105                 else if (trigger)
106                         return (Levenshtein(message, historyline) <= trigger);
107
108                 return false;
109         }
110
111         unsigned int Levenshtein(const std::string& s1, const std::string& s2)
112         {
113                 unsigned int l1 = s1.size();
114                 unsigned int l2 = s2.size();
115
116                 for (unsigned int i = 0; i < l2; i++)
117                         mx[0][i] = i;
118                 for (unsigned int i = 0; i < l1; i++)
119                 {
120                         mx[1][0] = i + 1;
121                         for (unsigned int j = 0; j < l2; j++)
122                                 mx[1][j + 1] = std::min(std::min(mx[1][j] + 1, mx[0][j + 1] + 1), mx[0][j] + ((s1[i] == s2[j]) ? 0 : 1));
123
124                         mx[0].swap(mx[1]);
125                 }
126                 return mx[0][l2];
127         }
128
129  public:
130         SimpleExtItem<MemberInfo> MemberInfoExt;
131
132         RepeatMode(Module* Creator)
133                 : ParamMode<RepeatMode, SimpleExtItem<ChannelSettings> >(Creator, "repeat", 'E')
134                 , MemberInfoExt("repeat_memb", ExtensionItem::EXT_MEMBERSHIP, Creator)
135         {
136                 syntax = "[~|*]<lines>:<sec>[:<difference>][:<backlog>]";
137         }
138
139         void OnUnset(User* source, Channel* chan) CXX11_OVERRIDE
140         {
141                 // Unset the per-membership extension when the mode is removed
142                 const Channel::MemberMap& users = chan->GetUsers();
143                 for (Channel::MemberMap::const_iterator i = users.begin(); i != users.end(); ++i)
144                         MemberInfoExt.unset(i->second);
145         }
146
147         ModeAction OnSet(User* source, Channel* channel, std::string& parameter) CXX11_OVERRIDE
148         {
149                 ChannelSettings settings;
150                 if (!ParseSettings(source, parameter, settings))
151                 {
152                         source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter));
153                         return MODEACTION_DENY;
154                 }
155
156                 if ((settings.Backlog > 0) && (settings.Lines > settings.Backlog))
157                 {
158                         source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter,
159                                 "You can't set lines higher than backlog."));
160                         return MODEACTION_DENY;
161                 }
162
163                 LocalUser* localsource = IS_LOCAL(source);
164                 if ((localsource) && (!ValidateSettings(localsource, channel, parameter, settings)))
165                         return MODEACTION_DENY;
166
167                 ext.set(channel, settings);
168
169                 return MODEACTION_ALLOW;
170         }
171
172         bool MatchLine(Membership* memb, ChannelSettings* rs, std::string message)
173         {
174                 // If the message is larger than whatever size it's set to,
175                 // let's pretend it isn't. If the first 512 (def. setting) match, it's probably spam.
176                 if (message.size() > ms.MaxMessageSize)
177                         message.erase(ms.MaxMessageSize);
178
179                 MemberInfo* rp = MemberInfoExt.get(memb);
180                 if (!rp)
181                 {
182                         rp = new MemberInfo;
183                         MemberInfoExt.set(memb, rp);
184                 }
185
186                 unsigned int matches = 0;
187                 if (!rs->Backlog)
188                         matches = rp->Counter;
189
190                 RepeatItemList& items = rp->ItemList;
191                 const unsigned int trigger = (message.size() * rs->Diff / 100);
192                 const time_t now = ServerInstance->Time();
193
194                 std::transform(message.begin(), message.end(), message.begin(), ::tolower);
195
196                 for (std::deque<RepeatItem>::iterator it = items.begin(); it != items.end(); ++it)
197                 {
198                         if (it->ts < now)
199                         {
200                                 items.erase(it, items.end());
201                                 matches = 0;
202                                 break;
203                         }
204
205                         if (CompareLines(message, it->line, trigger))
206                         {
207                                 if (++matches >= rs->Lines)
208                                 {
209                                         if (rs->Action != ChannelSettings::ACT_BLOCK)
210                                                 rp->Counter = 0;
211                                         return true;
212                                 }
213                         }
214                         else if ((ms.MaxBacklog == 0) || (rs->Backlog == 0))
215                         {
216                                 matches = 0;
217                                 items.clear();
218                                 break;
219                         }
220                 }
221
222                 unsigned int max_items = (rs->Backlog ? rs->Backlog : 1);
223                 if (items.size() >= max_items)
224                         items.pop_back();
225
226                 items.push_front(RepeatItem(now + rs->Seconds, message));
227                 rp->Counter = matches;
228                 return false;
229         }
230
231         void Resize(size_t size)
232         {
233                 size_t newsize = size+1;
234                 if (newsize <= mx[0].size())
235                         return;
236                 ms.MaxMessageSize = size;
237                 mx[0].resize(newsize);
238                 mx[1].resize(newsize);
239         }
240
241         void ReadConfig()
242         {
243                 ConfigTag* conf = ServerInstance->Config->ConfValue("repeat");
244                 ms.MaxLines = conf->getUInt("maxlines", 20);
245                 ms.MaxBacklog = conf->getUInt("maxbacklog", 20);
246                 ms.MaxSecs = conf->getDuration("maxtime", conf->getDuration("maxsecs", 0));
247
248                 ms.MaxDiff = conf->getUInt("maxdistance", 50);
249                 if (ms.MaxDiff > 100)
250                         ms.MaxDiff = 100;
251
252                 unsigned int newsize = conf->getUInt("size", 512);
253                 if (newsize > ServerInstance->Config->Limits.MaxLine)
254                         newsize = ServerInstance->Config->Limits.MaxLine;
255                 Resize(newsize);
256
257                 ms.KickMessage = conf->getString("kickmessage", "Repeat flood");
258         }
259
260         std::string GetModuleSettings() const
261         {
262                 return ConvToStr(ms.MaxLines) + ":" + ConvToStr(ms.MaxSecs) + ":" + ConvToStr(ms.MaxDiff) + ":" + ConvToStr(ms.MaxBacklog);
263         }
264
265         std::string GetKickMessage() const
266         {
267                 return ms.KickMessage;
268         }
269
270         void SerializeParam(Channel* chan, const ChannelSettings* chset, std::string& out)
271         {
272                 chset->serialize(out);
273         }
274
275  private:
276         bool ParseSettings(User* source, std::string& parameter, ChannelSettings& settings)
277         {
278                 irc::sepstream stream(parameter, ':');
279                 std::string     item;
280                 if (!stream.GetToken(item))
281                         // Required parameter missing
282                         return false;
283
284                 if ((item[0] == '*') || (item[0] == '~'))
285                 {
286                         settings.Action = ((item[0] == '*') ? ChannelSettings::ACT_BAN : ChannelSettings::ACT_BLOCK);
287                         item.erase(item.begin());
288                 }
289                 else
290                         settings.Action = ChannelSettings::ACT_KICK;
291
292                 if ((settings.Lines = ConvToNum<unsigned int>(item)) == 0)
293                         return false;
294
295                 if ((!stream.GetToken(item)) || !InspIRCd::Duration(item, settings.Seconds) || (settings.Seconds == 0))
296                         // Required parameter missing
297                         return false;
298
299                 // The diff and backlog parameters are optional
300                 settings.Diff = settings.Backlog = 0;
301                 if (stream.GetToken(item))
302                 {
303                         // There is a diff parameter, see if it's valid (> 0)
304                         if ((settings.Diff = ConvToNum<unsigned int>(item)) == 0)
305                                 return false;
306
307                         if (stream.GetToken(item))
308                         {
309                                 // There is a backlog parameter, see if it's valid
310                                 if ((settings.Backlog = ConvToNum<unsigned int>(item)) == 0)
311                                         return false;
312
313                                 // If there are still tokens, then it's invalid because we allow only 4
314                                 if (stream.GetToken(item))
315                                         return false;
316                         }
317                 }
318
319                 return true;
320         }
321
322         bool ValidateSettings(LocalUser* source, Channel* channel, const std::string& parameter, const ChannelSettings& settings)
323         {
324                 if (ms.MaxLines && settings.Lines > ms.MaxLines)
325                 {
326                         source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
327                                 "The line number you specified is too big. Maximum allowed is %u.", ms.MaxLines)));
328                         return false;
329                 }
330
331                 if (ms.MaxSecs && settings.Seconds > ms.MaxSecs)
332                 {
333                         source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
334                                 "The seconds you specified are too big. Maximum allowed is %u.", ms.MaxSecs)));
335                         return false;
336                 }
337
338                 if (settings.Diff && settings.Diff > ms.MaxDiff)
339                 {
340                         if (ms.MaxDiff == 0)
341                                 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter,
342                                         "The server administrator has disabled matching on edit distance."));
343                         else
344                                 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
345                                         "The distance you specified is too big. Maximum allowed is %u.", ms.MaxDiff)));
346                         return false;
347                 }
348
349                 if (settings.Backlog && settings.Backlog > ms.MaxBacklog)
350                 {
351                         if (ms.MaxBacklog == 0)
352                                 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter,
353                                         "The server administrator has disabled backlog matching."));
354                         else
355                                 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
356                                         "The backlog you specified is too big. Maximum allowed is %u.", ms.MaxBacklog)));
357                         return false;
358                 }
359
360                 return true;
361         }
362 };
363
364 class RepeatModule : public Module
365 {
366         CheckExemption::EventProvider exemptionprov;
367         RepeatMode rm;
368
369  public:
370         RepeatModule()
371                 : exemptionprov(this)
372                 , rm(this)
373         {
374         }
375
376         void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE
377         {
378                 rm.ReadConfig();
379         }
380
381         ModResult OnUserPreMessage(User* user, const MessageTarget& target, MessageDetails& details) CXX11_OVERRIDE
382         {
383                 if (target.type != MessageTarget::TYPE_CHANNEL || !IS_LOCAL(user))
384                         return MOD_RES_PASSTHRU;
385
386                 Channel* chan = target.Get<Channel>();
387                 ChannelSettings* settings = rm.ext.get(chan);
388                 if (!settings)
389                         return MOD_RES_PASSTHRU;
390
391                 Membership* memb = chan->GetUser(user);
392                 if (!memb)
393                         return MOD_RES_PASSTHRU;
394
395                 ModResult res = CheckExemption::Call(exemptionprov, user, chan, "repeat");
396                 if (res == MOD_RES_ALLOW)
397                         return MOD_RES_PASSTHRU;
398
399                 if (rm.MatchLine(memb, settings, details.text))
400                 {
401                         if (settings->Action == ChannelSettings::ACT_BLOCK)
402                         {
403                                 user->WriteNotice("*** This line is too similar to one of your last lines.");
404                                 return MOD_RES_DENY;
405                         }
406
407                         if (settings->Action == ChannelSettings::ACT_BAN)
408                         {
409                                 Modes::ChangeList changelist;
410                                 changelist.push_add(ServerInstance->Modes->FindMode('b', MODETYPE_CHANNEL), "*!*@" + user->GetDisplayedHost());
411                                 ServerInstance->Modes->Process(ServerInstance->FakeClient, chan, NULL, changelist);
412                         }
413
414                         memb->chan->KickUser(ServerInstance->FakeClient, user, rm.GetKickMessage());
415                         return MOD_RES_DENY;
416                 }
417                 return MOD_RES_PASSTHRU;
418         }
419
420         void Prioritize() CXX11_OVERRIDE
421         {
422                 ServerInstance->Modules->SetPriority(this, I_OnUserPreMessage, PRIORITY_LAST);
423         }
424
425         Version GetVersion() CXX11_OVERRIDE
426         {
427                 return Version("Adds channel mode E (repeat) which helps protect against spammers which spam the same message repeatedly.", VF_COMMON|VF_VENDOR, rm.GetModuleSettings());
428         }
429 };
430
431 MODULE_INIT(RepeatModule)