diff options
| author | 2024-07-17 00:06:50 +0100 | |
|---|---|---|
| committer | 2024-07-17 00:06:50 +0100 | |
| commit | 889d521e05f2e922683d48c74eb00290e7a2f548 (patch) | |
| tree | b4c56bd4cdf0881601c7e4d6ff571e491af10bf4 /modules/repeat.cpp | |
| parent | Use std::endian from C++20 in the sha1 module. (diff) | |
Shuffle the modules about a bit.
Diffstat (limited to 'modules/repeat.cpp')
| -rw-r--r-- | modules/repeat.cpp | 533 |
1 files changed, 533 insertions, 0 deletions
diff --git a/modules/repeat.cpp b/modules/repeat.cpp new file mode 100644 index 000000000..40dc36dda --- /dev/null +++ b/modules/repeat.cpp @@ -0,0 +1,533 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2021 Dominic Hamon + * Copyright (C) 2021 David Schultz <me@zpld.me> + * Copyright (C) 2018 linuxdaemon <linuxdaemon.irc@gmail.com> + * Copyright (C) 2018 Matt Schatz <genius3000@g3k.solutions> + * Copyright (C) 2017-2024 Sadie Powell <sadie@witchery.services> + * Copyright (C) 2013-2014 Attila Molnar <attilamolnar@hush.com> + * Copyright (C) 2013 Daniel Vassdal <shutter@canternet.org> + * + * This file is part of InspIRCd. InspIRCd is free software: you can + * redistribute it and/or modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation, version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + + +#include "inspircd.h" +#include "extension.h" +#include "modules/exemption.h" +#include "numerichelper.h" +#include "timeutils.h" + +class ChannelSettings final +{ +public: + enum RepeatAction + { + ACT_BAN, + ACT_BLOCK, + ACT_KICK, + ACT_KICK_BAN, + ACT_MUTE, + }; + + RepeatAction Action; + unsigned int Backlog; + unsigned int Lines; + unsigned int Diff; + unsigned long Seconds; + + void serialize(bool extended, std::string& out) const + { + if (extended) + { + switch (Action) + { + case ACT_BAN: + out.append("ban"); + break; + case ACT_BLOCK: + out.append("block"); + break; + case ACT_MUTE: + out.append("mute"); + break; + case ACT_KICK: + out.append("kick"); + break; + case ACT_KICK_BAN: + out.append("kickban"); + break; + } + out.push_back(':'); + } + else + { + switch (Action) + { + case ACT_KICK_BAN: + out.push_back('*'); + break; + + case ACT_BLOCK: + out.push_back('~'); + break; + + default: + break; // No other types are supported in the old mode. + } + } + + out.append(ConvToStr(Lines)).push_back(':'); + out.append(ConvToStr(Seconds)); + if (Diff) + { + out.push_back(':'); + out.append(ConvToStr(Diff)); + if (Backlog) + { + out.push_back(':'); + out.append(ConvToStr(Backlog)); + } + } + } +}; + +class RepeatMode final + : public ParamMode<RepeatMode, SimpleExtItem<ChannelSettings>> +{ +private: + struct RepeatItem final + { + time_t ts; + std::string line; + RepeatItem(time_t TS, const std::string& Line) + : ts(TS) + , line(Line) + { + } + }; + + typedef std::deque<RepeatItem> RepeatItemList; + + struct MemberInfo final + { + RepeatItemList ItemList; + unsigned int Counter = 0; + }; + + struct ModuleSettings final + { + bool Extended = false; + unsigned long MaxLines = 0; + unsigned long MaxSecs = 0; + unsigned long MaxBacklog = 0; + unsigned int MaxDiff = 0; + size_t MaxMessageSize = 0; + std::string Message; + }; + + std::vector<size_t> mx[2]; + + bool CompareLines(const std::string& message, const std::string& historyline, unsigned long trigger) + { + if (message == historyline) + return true; + else if (trigger) + return (Levenshtein(message, historyline) <= trigger); + + return false; + } + + size_t Levenshtein(const std::string& s1, const std::string& s2) + { + size_t l1 = s1.size(); + size_t l2 = s2.size(); + + for (size_t i = 0; i < l2; i++) + mx[0][i] = i; + for (size_t i = 0; i < l1; i++) + { + mx[1][0] = i + 1; + for (size_t j = 0; j < l2; j++) + 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)); + + mx[0].swap(mx[1]); + } + return mx[0][l2]; + } + +public: + ModuleSettings ms; + SimpleExtItem<MemberInfo> MemberInfoExt; + + RepeatMode(Module* Creator) + : ParamMode<RepeatMode, SimpleExtItem<ChannelSettings>>(Creator, "repeat", 'E') + , MemberInfoExt(Creator, "repeat", ExtensionType::MEMBERSHIP) + { + } + + void OnUnset(User* source, Channel* chan) override + { + // Unset the per-membership extension when the mode is removed + for (const auto& [_, memb] : chan->GetUsers()) + MemberInfoExt.Unset(memb); + } + + bool OnSet(User* source, Channel* channel, std::string& parameter) override + { + ChannelSettings settings; + if (!ParseSettings(source, parameter, settings)) + { + source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter)); + return false; + } + + if ((settings.Backlog > 0) && (settings.Lines > settings.Backlog)) + { + source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, + "You can't set lines higher than backlog.")); + return false; + } + + LocalUser* localsource = IS_LOCAL(source); + if ((localsource) && (!ValidateSettings(localsource, channel, parameter, settings))) + return false; + + ext.Set(channel, settings); + + return true; + } + + bool MatchLine(Membership* memb, ChannelSettings* rs, std::string message) + { + // If the message is larger than whatever size it's set to, + // let's pretend it isn't. If the first 512 (def. setting) match, it's probably spam. + if (message.size() > ms.MaxMessageSize) + message.erase(ms.MaxMessageSize); + + MemberInfo* rp = MemberInfoExt.Get(memb); + if (!rp) + { + rp = new MemberInfo; + MemberInfoExt.Set(memb, rp); + } + + unsigned int matches = 0; + if (!rs->Backlog) + matches = rp->Counter; + + RepeatItemList& items = rp->ItemList; + const unsigned long trigger = (message.size() * rs->Diff / 100); + const time_t now = ServerInstance->Time(); + + std::transform(message.begin(), message.end(), message.begin(), ::tolower); + + for (std::deque<RepeatItem>::iterator it = items.begin(); it != items.end(); ++it) + { + if (it->ts < now) + { + items.erase(it, items.end()); + matches = 0; + break; + } + + if (CompareLines(message, it->line, trigger)) + { + if (++matches >= rs->Lines) + { + if (rs->Action != ChannelSettings::ACT_BLOCK) + rp->Counter = 0; + return true; + } + } + else if ((ms.MaxBacklog == 0) || (rs->Backlog == 0)) + { + matches = 0; + items.clear(); + break; + } + } + + unsigned int max_items = (rs->Backlog ? rs->Backlog : 1); + if (items.size() >= max_items) + items.pop_back(); + + items.push_front(RepeatItem(now + rs->Seconds, message)); + rp->Counter = matches; + return false; + } + + void Resize(size_t size) + { + size_t newsize = size+1; + if (newsize <= mx[0].size()) + return; + ms.MaxMessageSize = size; + mx[0].resize(newsize); + mx[1].resize(newsize); + } + + void SerializeParam(Channel* chan, const ChannelSettings* chset, std::string& out) + { + chset->serialize(ms.Extended, out); + } + + void SetSyntax() + { + if (ms.Extended) + syntax = "{ban|block|mute|kick|kickban}:<lines>:<duration>[:<difference>][:<backlog>]"; + else + syntax = "[~|*]<lines>:<duration>[:<difference>][:<backlog>]"; + + } + +private: + bool ParseAction(irc::sepstream& stream, ChannelSettings::RepeatAction& action) + { + std::string actionstr; + if (!stream.GetToken(actionstr)) + return false; + + if (irc::equals(actionstr, "ban")) + action = ChannelSettings::ACT_BAN; + else if (irc::equals(actionstr, "kick")) + action = ChannelSettings::ACT_KICK; + else if (irc::equals(actionstr, "block")) + action = ChannelSettings::ACT_BLOCK; + else if (irc::equals(actionstr, "mute")) + action = ChannelSettings::ACT_MUTE; + else if (irc::equals(actionstr, "kickban")) + action = ChannelSettings::ACT_KICK_BAN; + else + return false; + + return true; + } + + bool ParseSettings(User* source, std::string& parameter, ChannelSettings& settings) + { + irc::sepstream stream(parameter, ':'); + std::string item; + + if (ms.Extended) + { + if (!ParseAction(stream, settings.Action) || !stream.GetToken(item)) + return false; + } + else + { + if (!stream.GetToken(item)) + return false; // Required parameter missing + + settings.Action = ChannelSettings::ACT_KICK; + if ((item[0] == '*') || (item[0] == '~')) + { + settings.Action = ((item[0] == '*') ? ChannelSettings::ACT_KICK_BAN : ChannelSettings::ACT_BLOCK); + item.erase(item.begin()); + } + } + + if ((settings.Lines = ConvToNum<unsigned int>(item)) == 0) + return false; + + if ((!stream.GetToken(item)) || !Duration::TryFrom(item, settings.Seconds) || (settings.Seconds == 0)) + // Required parameter missing + return false; + + // The diff and backlog parameters are optional + settings.Diff = settings.Backlog = 0; + if (stream.GetToken(item)) + { + // There is a diff parameter, see if it's valid (> 0) + if ((settings.Diff = ConvToNum<unsigned int>(item)) == 0) + return false; + + if (stream.GetToken(item)) + { + // There is a backlog parameter, see if it's valid + if ((settings.Backlog = ConvToNum<unsigned int>(item)) == 0) + return false; + + // If there are still tokens, then it's invalid because we allow only 4 + if (stream.GetToken(item)) + return false; + } + } + + return true; + } + + bool ValidateSettings(LocalUser* source, Channel* channel, const std::string& parameter, const ChannelSettings& settings) + { + if (ms.MaxLines && settings.Lines > ms.MaxLines) + { + source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, fmt::format( + "The line number you specified is too big. Maximum allowed is {}.", ms.MaxLines))); + return false; + } + + if (ms.MaxSecs && settings.Seconds > ms.MaxSecs) + { + source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, fmt::format( + "The seconds you specified are too big. Maximum allowed is {}.", ms.MaxSecs))); + return false; + } + + if (settings.Diff && settings.Diff > ms.MaxDiff) + { + if (ms.MaxDiff == 0) + source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, + "The server administrator has disabled matching on edit distance.")); + else + source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, fmt::format( + "The distance you specified is too big. Maximum allowed is {}.", ms.MaxDiff))); + return false; + } + + if (settings.Backlog && settings.Backlog > ms.MaxBacklog) + { + if (ms.MaxBacklog == 0) + source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, + "The server administrator has disabled backlog matching.")); + else + source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, fmt::format( + "The backlog you specified is too big. Maximum allowed is {}.", ms.MaxBacklog))); + return false; + } + + return true; + } +}; + +class RepeatModule final + : public Module +{ +private: + ChanModeReference banmode; + CheckExemption::EventProvider exemptionprov; + RepeatMode rm; + + void CreateBan(Channel* channel, User* user, bool mute) + { + std::string banmask(mute ? "m:*!" : "*!"); + banmask.append(user->GetBanUser(false)); + banmask.append("@"); + banmask.append(user->GetDisplayedHost()); + + Modes::ChangeList changelist; + changelist.push_add(*banmode, banmask); + ServerInstance->Modes.Process(ServerInstance->FakeClient, channel, nullptr, changelist); + } + +public: + RepeatModule() + : Module(VF_VENDOR | VF_COMMON, "Adds channel mode E (repeat) which helps protect against spammers which spam the same message repeatedly.") + , banmode(this, "ban") + , exemptionprov(this) + , rm(this) + { + } + + void ReadConfig(ConfigStatus& status) override + { + const auto& tag = ServerInstance->Config->ConfValue("repeat"); + rm.ms.Message = tag->getString("message", "Repeat flood (trigger is %lines% messages in %duration%)", 1); + rm.ms.Extended = tag->getBool("extended"); + rm.ms.MaxBacklog = tag->getNum<unsigned long>("maxbacklog", 20); + rm.ms.MaxDiff = tag->getNum<unsigned int>("maxdistance", 50, 0, 100); + rm.ms.MaxLines = tag->getNum<unsigned long>("maxlines", 20); + rm.ms.MaxSecs = tag->getDuration("maxtime", 0); + + rm.Resize(tag->getNum<size_t>("size", 512, 1, ServerInstance->Config->Limits.MaxLine)); + } + + ModResult OnUserPreMessage(User* user, MessageTarget& target, MessageDetails& details) override + { + if (target.type != MessageTarget::TYPE_CHANNEL || !IS_LOCAL(user)) + return MOD_RES_PASSTHRU; + + auto* chan = target.Get<Channel>(); + ChannelSettings* settings = rm.ext.Get(chan); + if (!settings) + return MOD_RES_PASSTHRU; + + Membership* memb = chan->GetUser(user); + if (!memb) + return MOD_RES_PASSTHRU; + + ModResult res = exemptionprov.Check(user, chan, "repeat"); + if (res == MOD_RES_ALLOW) + return MOD_RES_PASSTHRU; + + if (user->HasPrivPermission("channels/ignore-repeat")) + return MOD_RES_PASSTHRU; + + if (rm.MatchLine(memb, settings, details.text)) + { + const std::string message = Template::Replace(rm.ms.Message, { + { "diff", ConvToStr(settings->Diff) }, + { "duration", Duration::ToString(settings->Seconds) }, + { "lines", ConvToStr(settings->Lines) }, + { "seconds", ConvToStr(settings->Seconds) }, + }); + + switch (settings->Action) + { + case ChannelSettings::ACT_BAN: + memb->WriteNotice(message); + CreateBan(chan, user, false); + break; + + case ChannelSettings::ACT_BLOCK: + memb->WriteNotice(message); + break; + + case ChannelSettings::ACT_KICK: + chan->KickUser(ServerInstance->FakeClient, user, message); + break; + + case ChannelSettings::ACT_KICK_BAN: + CreateBan(chan, user, false); + chan->KickUser(ServerInstance->FakeClient, user, message); + break; + + case ChannelSettings::ACT_MUTE: + memb->WriteNotice(message); + CreateBan(chan, user, true); + break; + } + return MOD_RES_DENY; + } + return MOD_RES_PASSTHRU; + } + + void Prioritize() override + { + ServerInstance->Modules.SetPriority(this, I_OnUserPreMessage, PRIORITY_LAST); + } + + void GetLinkData(LinkData& data, std::string& compatdata) override + { + data["actions"] = rm.ms.Extended ? "ban block kick kickban mute" : "block kick kickban"; + data["max-lines"] = ConvToStr(rm.ms.MaxLines); + data["max-secs"] = ConvToStr(rm.ms.MaxSecs); + data["max-diff"] = ConvToStr(rm.ms.MaxDiff); + data["max-backlog"] = ConvToStr(rm.ms.MaxBacklog); + + compatdata = fmt::format("{}:{}:{}:{}{}", rm.ms.Extended ? "extended:" : "", + rm.ms.MaxLines, rm.ms.MaxSecs, rm.ms.MaxDiff, rm.ms.MaxBacklog); + } +}; + +MODULE_INIT(RepeatModule) |
