/* * InspIRCd -- Internet Relay Chat Daemon * * Copyright (C) 2013 Daniel Vassdal * * 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 . */ #include "inspircd.h" #include "modules/exemption.h" class ChannelSettings { public: enum RepeatAction { ACT_KICK, ACT_BLOCK, ACT_BAN }; RepeatAction Action; unsigned int Backlog; unsigned int Lines; unsigned int Diff; unsigned int Seconds; void serialize(std::string& out) const { if (Action == ACT_BAN) out.push_back('*'); else if (Action == ACT_BLOCK) out.push_back('~'); 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 : public ParamMode > { private: struct RepeatItem { time_t ts; std::string line; RepeatItem(time_t TS, const std::string& Line) : ts(TS), line(Line) { } }; typedef std::deque RepeatItemList; struct MemberInfo { RepeatItemList ItemList; unsigned int Counter; MemberInfo() : Counter(0) {} }; struct ModuleSettings { unsigned int MaxLines; unsigned int MaxSecs; unsigned int MaxBacklog; unsigned int MaxDiff; unsigned int MaxMessageSize; ModuleSettings() : MaxLines(0), MaxSecs(0), MaxBacklog(0), MaxDiff() { } }; std::vector mx[2]; ModuleSettings ms; bool CompareLines(const std::string& message, const std::string& historyline, unsigned int trigger) { if (message == historyline) return true; else if (trigger) return (Levenshtein(message, historyline) <= trigger); return false; } unsigned int Levenshtein(const std::string& s1, const std::string& s2) { unsigned int l1 = s1.size(); unsigned int l2 = s2.size(); for (unsigned int i = 0; i < l2; i++) mx[0][i] = i; for (unsigned int i = 0; i < l1; i++) { mx[1][0] = i + 1; for (unsigned int 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: SimpleExtItem MemberInfoExt; RepeatMode(Module* Creator) : ParamMode >(Creator, "repeat", 'E') , MemberInfoExt("repeat_memb", ExtensionItem::EXT_MEMBERSHIP, Creator) { } void OnUnset(User* source, Channel* chan) { // Unset the per-membership extension when the mode is removed const Channel::MemberMap& users = chan->GetUsers(); for (Channel::MemberMap::const_iterator i = users.begin(); i != users.end(); ++i) MemberInfoExt.unset(i->second); } ModeAction OnSet(User* source, Channel* channel, std::string& parameter) { ChannelSettings settings; if (!ParseSettings(source, parameter, settings)) { source->WriteNotice("*** Invalid syntax. Syntax is {[~*]}[lines]:[time]{:[difference]}{:[backlog]}"); return MODEACTION_DENY; } if ((settings.Backlog > 0) && (settings.Lines > settings.Backlog)) { source->WriteNotice("*** You can't set needed lines higher than backlog"); return MODEACTION_DENY; } LocalUser* localsource = IS_LOCAL(source); if ((localsource) && (!ValidateSettings(localsource, settings))) return MODEACTION_DENY; ext.set(channel, settings); return MODEACTION_ALLOW; } 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 int trigger = (message.size() * rs->Diff / 100); const time_t now = ServerInstance->Time(); std::transform(message.begin(), message.end(), message.begin(), ::tolower); for (std::deque::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 ReadConfig() { ConfigTag* conf = ServerInstance->Config->ConfValue("repeat"); ms.MaxLines = conf->getInt("maxlines", 20); ms.MaxBacklog = conf->getInt("maxbacklog", 20); ms.MaxSecs = conf->getDuration("maxtime", conf->getInt("maxsecs", 0)); ms.MaxDiff = conf->getInt("maxdistance", 50); if (ms.MaxDiff > 100) ms.MaxDiff = 100; unsigned int newsize = conf->getInt("size", 512); if (newsize > ServerInstance->Config->Limits.MaxLine) newsize = ServerInstance->Config->Limits.MaxLine; Resize(newsize); } std::string GetModuleSettings() const { return ConvToStr(ms.MaxLines) + ":" + ConvToStr(ms.MaxSecs) + ":" + ConvToStr(ms.MaxDiff) + ":" + ConvToStr(ms.MaxBacklog); } void SerializeParam(Channel* chan, const ChannelSettings* chset, std::string& out) { chset->serialize(out); } private: bool ParseSettings(User* source, std::string& parameter, ChannelSettings& settings) { irc::sepstream stream(parameter, ':'); std::string item; if (!stream.GetToken(item)) // Required parameter missing return false; if ((item[0] == '*') || (item[0] == '~')) { settings.Action = ((item[0] == '*') ? ChannelSettings::ACT_BAN : ChannelSettings::ACT_BLOCK); item.erase(item.begin()); } else settings.Action = ChannelSettings::ACT_KICK; if ((settings.Lines = ConvToInt(item)) == 0) return false; if ((!stream.GetToken(item)) || ((settings.Seconds = InspIRCd::Duration(item)) == 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 = ConvToInt(item)) == 0) return false; if (stream.GetToken(item)) { // There is a backlog parameter, see if it's valid if ((settings.Backlog = ConvToInt(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, const ChannelSettings& settings) { if (settings.Backlog && !ms.MaxBacklog) { source->WriteNotice("*** The server administrator has disabled backlog matching"); return false; } if (settings.Diff) { if (settings.Diff > ms.MaxDiff) { if (ms.MaxDiff == 0) source->WriteNotice("*** The server administrator has disabled matching on edit distance"); else source->WriteNotice("*** The distance you specified is too great. Maximum allowed is " + ConvToStr(ms.MaxDiff)); return false; } if (ms.MaxLines && settings.Lines > ms.MaxLines) { source->WriteNotice("*** The line number you specified is too great. Maximum allowed is " + ConvToStr(ms.MaxLines)); return false; } if (ms.MaxSecs && settings.Seconds > ms.MaxSecs) { source->WriteNotice("*** The seconds you specified is too great. Maximum allowed is " + ConvToStr(ms.MaxSecs)); return false; } } return true; } }; class RepeatModule : public Module { CheckExemption::EventProvider exemptionprov; RepeatMode rm; public: RepeatModule() : exemptionprov(this) , rm(this) { } void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE { rm.ReadConfig(); } ModResult OnUserPreMessage(User* user, void* dest, int target_type, std::string& text, char status, CUList& exempt_list, MessageType msgtype) CXX11_OVERRIDE { if (target_type != TYPE_CHANNEL || !IS_LOCAL(user)) return MOD_RES_PASSTHRU; Channel* chan = reinterpret_cast(dest); 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; FIRST_MOD_RESULT_CUSTOM(exemptionprov, CheckExemption::EventListener, OnCheckExemption, res, (user, chan, "repeat")); if (res == MOD_RES_ALLOW) return MOD_RES_PASSTHRU; if (rm.MatchLine(memb, settings, text)) { if (settings->Action == ChannelSettings::ACT_BLOCK) { user->WriteNotice("*** This line is too similar to one of your last lines."); return MOD_RES_DENY; } if (settings->Action == ChannelSettings::ACT_BAN) { Modes::ChangeList changelist; changelist.push_add(ServerInstance->Modes->FindMode('b', MODETYPE_CHANNEL), "*!*@" + user->dhost); ServerInstance->Modes->Process(ServerInstance->FakeClient, chan, NULL, changelist); } memb->chan->KickUser(ServerInstance->FakeClient, user, "Repeat flood"); return MOD_RES_DENY; } return MOD_RES_PASSTHRU; } void Prioritize() CXX11_OVERRIDE { ServerInstance->Modules->SetPriority(this, I_OnUserPreMessage, PRIORITY_LAST); } Version GetVersion() CXX11_OVERRIDE { return Version("Provides the +E channel mode - for blocking of similar messages", VF_COMMON|VF_VENDOR, rm.GetModuleSettings()); } }; MODULE_INIT(RepeatModule)