]> git.netwichtig.de Git - user/henk/code/inspircd.git/blobdiff - src/modules/m_filter.cpp
Add a config option for exempting classes from connflood.
[user/henk/code/inspircd.git] / src / modules / m_filter.cpp
index 3609d0d1019352e3925992533c060cc4cb827c9d..786ea673bb27cce92881c678b42e01ba7cedda5b 100644 (file)
-/*       +------------------------------------+
- *       | Inspire Internet Relay Chat Daemon |
- *       +------------------------------------+
+/*
+ * InspIRCd -- Internet Relay Chat Daemon
  *
- *  InspIRCd: (C) 2002-2008 InspIRCd Development Team
- * See: http://www.inspircd.org/wiki/index.php/Credits
+ *   Copyright (C) 2019 Matt Schatz <genius3000@g3k.solutions>
+ *   Copyright (C) 2019 Filippo Cortigiani <simos@simosnap.org>
+ *   Copyright (C) 2018-2019 linuxdaemon <linuxdaemon.irc@gmail.com>
+ *   Copyright (C) 2018 Michael Hazell <michaelhazell@hotmail.com>
+ *   Copyright (C) 2017 B00mX0r <b00mx0r@aureus.pw>
+ *   Copyright (C) 2012-2014, 2016 Attila Molnar <attilamolnar@hush.com>
+ *   Copyright (C) 2012-2013, 2017-2020 Sadie Powell <sadie@witchery.services>
+ *   Copyright (C) 2012, 2018-2019 Robby <robby@chatbelgie.be>
+ *   Copyright (C) 2011 Adam <Adam@anope.org>
+ *   Copyright (C) 2009-2010 Daniel De Graaf <danieldg@inspircd.org>
+ *   Copyright (C) 2009 Robin Burchell <robin+git@viroteck.net>
+ *   Copyright (C) 2009 Matt Smith <dz@inspircd.org>
+ *   Copyright (C) 2009 John Brooks <special@inspircd.org>
+ *   Copyright (C) 2007-2010 Craig Edwards <brain@inspircd.org>
+ *   Copyright (C) 2007 Dennis Friis <peavey@inspircd.org>
  *
- * This program is free but copyrighted software; see
- *            the file COPYING for details.
+ * 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 "m_filter.h"
+#include "xline.h"
+#include "modules/regex.h"
+#include "modules/server.h"
+#include "modules/shun.h"
+#include "modules/stats.h"
+#include "modules/account.h"
 
-/* $ModDesc: An advanced spam filtering module */
-/* $ModDep: m_filter.h */
+enum FilterFlags
+{
+       FLAG_PART = 2,
+       FLAG_QUIT = 4,
+       FLAG_PRIVMSG = 8,
+       FLAG_NOTICE = 16
+};
 
-typedef std::map<std::string,FilterResult*> filter_t;
+enum FilterAction
+{
+       FA_GLINE,
+       FA_ZLINE,
+       FA_WARN,
+       FA_BLOCK,
+       FA_SILENT,
+       FA_KILL,
+       FA_SHUN,
+       FA_NONE
+};
 
-class ModuleFilter : public FilterBase
+class FilterResult
 {
+ public:
+       Regex* regex;
+       std::string freeform;
+       std::string reason;
+       FilterAction action;
+       unsigned long duration;
+       bool from_config;
+
+       bool flag_no_opers;
+       bool flag_part_message;
+       bool flag_quit_message;
+       bool flag_privmsg;
+       bool flag_notice;
+       bool flag_strip_color;
+       bool flag_no_registered;
+
+       FilterResult(dynamic_reference<RegexFactory>& RegexEngine, const std::string& free, const std::string& rea, FilterAction act, unsigned long gt, const std::string& fla, bool cfg)
+               : freeform(free)
+               , reason(rea)
+               , action(act)
+               , duration(gt)
+               , from_config(cfg)
+       {
+               if (!RegexEngine)
+                       throw ModuleException("Regex module implementing '"+RegexEngine.GetProvider()+"' is not loaded!");
+               regex = RegexEngine->Create(free);
+               this->FillFlags(fla);
+       }
+
+       char FillFlags(const std::string &fl)
+       {
+               flag_no_opers = flag_part_message = flag_quit_message = flag_privmsg =
+                       flag_notice = flag_strip_color = flag_no_registered = false;
+
+               for (std::string::const_iterator n = fl.begin(); n != fl.end(); ++n)
+               {
+                       switch (*n)
+                       {
+                               case 'o':
+                                       flag_no_opers = true;
+                               break;
+                               case 'P':
+                                       flag_part_message = true;
+                               break;
+                               case 'q':
+                                       flag_quit_message = true;
+                               break;
+                               case 'p':
+                                       flag_privmsg = true;
+                               break;
+                               case 'n':
+                                       flag_notice = true;
+                               break;
+                               case 'c':
+                                       flag_strip_color = true;
+                               break;
+                               case 'r':
+                                       flag_no_registered = true;
+                               break;
+                               case '*':
+                                       flag_no_opers = flag_part_message = flag_quit_message =
+                                               flag_privmsg = flag_notice = flag_strip_color = true;
+                               break;
+                               default:
+                                       return *n;
+                               break;
+                       }
+               }
+               return 0;
+       }
+
+       std::string GetFlags()
+       {
+               std::string flags;
+               if (flag_no_opers)
+                       flags.push_back('o');
+               if (flag_part_message)
+                       flags.push_back('P');
+               if (flag_quit_message)
+                       flags.push_back('q');
+               if (flag_privmsg)
+                       flags.push_back('p');
+               if (flag_notice)
+                       flags.push_back('n');
+
+               /* Order is important here, as the logic in FillFlags() stops parsing when it encounters
+                * an unknown character. So the following characters must be last in the string.
+                * 'c' is unsupported on < 2.0.10
+                * 'r' is unsupported on < 3.2.0
+                */
+               if (flag_strip_color)
+                       flags.push_back('c');
+               if (flag_no_registered)
+                       flags.push_back('r');
+
+               if (flags.empty())
+                       flags.push_back('-');
 
- filter_t filters;
+               return flags;
+       }
+
+       FilterResult()
+       {
+       }
+};
 
+class CommandFilter : public Command
+{
  public:
-       ModuleFilter(InspIRCd* Me)
-       : FilterBase(Me, "m_filter.so")
+       CommandFilter(Module* f)
+               : Command(f, "FILTER", 1, 5)
        {
-               OnRehash(NULL,"");
+               flags_needed = 'o';
+               this->syntax = "<pattern> [<action> <flags> [<duration>] :<reason>]";
+       }
+       CmdResult Handle(User* user, const Params& parameters) CXX11_OVERRIDE;
 
+       RouteDescriptor GetRouting(User* user, const Params& parameters) CXX11_OVERRIDE
+       {
+               return ROUTE_BROADCAST;
        }
+};
+
+class ModuleFilter
+       : public Module
+       , public ServerProtocol::SyncEventListener
+       , public Stats::EventListener
+{
+       typedef insp::flat_set<std::string, irc::insensitive_swo> ExemptTargetSet;
+
+       bool initing;
+       bool notifyuser;
+       bool warnonselfmsg;
+       RegexFactory* factory;
+       void FreeFilters();
+
+ public:
+       CommandFilter filtcommand;
+       dynamic_reference<RegexFactory> RegexEngine;
+
+       std::vector<FilterResult> filters;
+       int flags;
+
+       // List of channel names excluded from filtering.
+       ExemptTargetSet exemptedchans;
 
-       virtual ~ModuleFilter()
+       // List of target nicknames excluded from filtering.
+       ExemptTargetSet exemptednicks;
+
+       ModuleFilter();
+       void init() CXX11_OVERRIDE;
+       CullResult cull() CXX11_OVERRIDE;
+       ModResult OnUserPreMessage(User* user, const MessageTarget& target, MessageDetails& details) CXX11_OVERRIDE;
+       FilterResult* FilterMatch(User* user, const std::string &text, int flags);
+       bool DeleteFilter(const std::string& freeform, std::string& reason);
+       std::pair<bool, std::string> AddFilter(const std::string& freeform, FilterAction type, const std::string& reason, unsigned long duration, const std::string& flags, bool config = false);
+       void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE;
+       Version GetVersion() CXX11_OVERRIDE;
+       std::string EncodeFilter(FilterResult* filter);
+       FilterResult DecodeFilter(const std::string &data);
+       void OnSyncNetwork(ProtocolInterface::Server& server) CXX11_OVERRIDE;
+       void OnDecodeMetaData(Extensible* target, const std::string &extname, const std::string &extdata) CXX11_OVERRIDE;
+       ModResult OnStats(Stats::Context& stats) CXX11_OVERRIDE;
+       ModResult OnPreCommand(std::string& command, CommandBase::Params& parameters, LocalUser* user, bool validated) CXX11_OVERRIDE;
+       void OnUnloadModule(Module* mod) CXX11_OVERRIDE;
+       bool AppliesToMe(User* user, FilterResult* filter, int flags);
+       void ReadFilters();
+       static bool StringToFilterAction(const std::string& str, FilterAction& fa);
+       static std::string FilterActionToString(FilterAction fa);
+};
+
+CmdResult CommandFilter::Handle(User* user, const Params& parameters)
+{
+       if (parameters.size() == 1)
        {
+               /* Deleting a filter */
+               Module* me = creator;
+               std::string reason;
+
+               if (static_cast<ModuleFilter*>(me)->DeleteFilter(parameters[0], reason))
+               {
+                       user->WriteNotice("*** Removed filter '" + parameters[0] + "': " + reason);
+                       ServerInstance->SNO->WriteToSnoMask(IS_LOCAL(user) ? 'f' : 'F', "%s removed filter '%s': %s",
+                               user->nick.c_str(), parameters[0].c_str(), reason.c_str());
+                       return CMD_SUCCESS;
+               }
+               else
+               {
+                       user->WriteNotice("*** Filter '" + parameters[0] + "' not found on the list.");
+                       return CMD_FAILURE;
+               }
+       }
+       else
+       {
+               /* Adding a filter */
+               if (parameters.size() >= 4)
+               {
+                       const std::string& freeform = parameters[0];
+                       FilterAction type;
+                       const std::string& flags = parameters[2];
+                       unsigned int reasonindex;
+                       unsigned long duration = 0;
+
+                       if (!ModuleFilter::StringToFilterAction(parameters[1], type))
+                       {
+                               if (ServerInstance->XLines->GetFactory("SHUN"))
+                                       user->WriteNotice("*** Invalid filter type '" + parameters[1] + "'. Supported types are 'gline', 'zline', 'none', 'warn', 'block', 'silent', 'kill', and 'shun'.");
+                               else
+                                       user->WriteNotice("*** Invalid filter type '" + parameters[1] + "'. Supported types are 'gline', 'zline', 'none', 'warn', 'block', 'silent', and 'kill'.");
+                               return CMD_FAILURE;
+                       }
+
+                       if (type == FA_GLINE || type == FA_ZLINE || type == FA_SHUN)
+                       {
+                               if (parameters.size() >= 5)
+                               {
+                                       if (!InspIRCd::Duration(parameters[3], duration))
+                                       {
+                                               user->WriteNotice("*** Invalid duration for filter");
+                                               return CMD_FAILURE;
+                                       }
+                                       reasonindex = 4;
+                               }
+                               else
+                               {
+                                       user->WriteNotice("*** Not enough parameters: When setting a '" + parameters[1] + "' type filter, a duration must be specified as the third parameter.");
+                                       return CMD_FAILURE;
+                               }
+                       }
+                       else
+                       {
+                               reasonindex = 3;
+                       }
+
+                       Module* me = creator;
+                       std::pair<bool, std::string> result = static_cast<ModuleFilter*>(me)->AddFilter(freeform, type, parameters[reasonindex], duration, flags);
+                       if (result.first)
+                       {
+                               const std::string message = InspIRCd::Format("'%s', type '%s'%s, flags '%s', reason: %s",
+                                       freeform.c_str(), parameters[1].c_str(),
+                                       (duration ? InspIRCd::Format(", duration '%s'",
+                                               InspIRCd::DurationString(duration).c_str()).c_str()
+                                       : ""), flags.c_str(), parameters[reasonindex].c_str());
+
+                               user->WriteNotice("*** Added filter " + message);
+                               ServerInstance->SNO->WriteToSnoMask(IS_LOCAL(user) ? 'f' : 'F',
+                                       "%s added filter %s", user->nick.c_str(), message.c_str());
+
+                               return CMD_SUCCESS;
+                       }
+                       else
+                       {
+                               user->WriteNotice("*** Filter '" + freeform + "' could not be added: " + result.second);
+                               return CMD_FAILURE;
+                       }
+               }
+               else
+               {
+                       user->WriteNotice("*** Not enough parameters.");
+                       return CMD_FAILURE;
+               }
+
+       }
+}
+
+bool ModuleFilter::AppliesToMe(User* user, FilterResult* filter, int iflags)
+{
+       const AccountExtItem* accountext = GetAccountExtItem();
+
+       if ((filter->flag_no_opers) && user->IsOper())
+               return false;
+       if ((filter->flag_no_registered) && accountext && accountext->get(user))
+               return false;
+       if ((iflags & FLAG_PRIVMSG) && (!filter->flag_privmsg))
+               return false;
+       if ((iflags & FLAG_NOTICE) && (!filter->flag_notice))
+               return false;
+       if ((iflags & FLAG_QUIT)   && (!filter->flag_quit_message))
+               return false;
+       if ((iflags & FLAG_PART)   && (!filter->flag_part_message))
+               return false;
+       return true;
+}
+
+ModuleFilter::ModuleFilter()
+       : ServerProtocol::SyncEventListener(this)
+       , Stats::EventListener(this)
+       , initing(true)
+       , filtcommand(this)
+       , RegexEngine(this, "regex")
+{
+}
+
+void ModuleFilter::init()
+{
+       ServerInstance->SNO->EnableSnomask('f', "FILTER");
+}
+
+CullResult ModuleFilter::cull()
+{
+       FreeFilters();
+       return Module::cull();
+}
+
+void ModuleFilter::FreeFilters()
+{
+       for (std::vector<FilterResult>::const_iterator i = filters.begin(); i != filters.end(); ++i)
+               delete i->regex;
+
+       filters.clear();
+}
+
+ModResult ModuleFilter::OnUserPreMessage(User* user, const MessageTarget& msgtarget, MessageDetails& details)
+{
+       // Leave remote users and servers alone
+       if (!IS_LOCAL(user))
+               return MOD_RES_PASSTHRU;
+
+       flags = (details.type == MSG_PRIVMSG) ? FLAG_PRIVMSG : FLAG_NOTICE;
+
+       FilterResult* f = this->FilterMatch(user, details.text, flags);
+       if (f)
+       {
+               bool is_selfmsg = false;
+               switch (msgtarget.type)
+               {
+                       case MessageTarget::TYPE_USER:
+                       {
+                               User* t = msgtarget.Get<User>();
+                               // Check if the target nick is exempted, if yes, ignore this message
+                               if (exemptednicks.count(t->nick))
+                                       return MOD_RES_PASSTHRU;
+
+                               if (user == t)
+                                       is_selfmsg = true;
+                               break;
+                       }
+                       case MessageTarget::TYPE_CHANNEL:
+                       {
+                               Channel* t = msgtarget.Get<Channel>();
+                               if (exemptedchans.count(t->name))
+                                       return MOD_RES_PASSTHRU;
+                               break;
+                       }
+                       case MessageTarget::TYPE_SERVER:
+                               return MOD_RES_PASSTHRU;
+               }
+
+               if (is_selfmsg && warnonselfmsg)
+               {
+                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("WARNING: %s's self message matched %s (%s)",
+                               user->nick.c_str(), f->freeform.c_str(), f->reason.c_str()));
+                       return MOD_RES_PASSTHRU;
+               }
+               else if (f->action == FA_WARN)
+               {
+                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("WARNING: %s's message to %s matched %s (%s)",
+                               user->nick.c_str(), msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
+                       return MOD_RES_PASSTHRU;
+               }
+               else if (f->action == FA_BLOCK)
+               {
+                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s had their message to %s filtered as it matched %s (%s)",
+                               user->nick.c_str(), msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
+                       if (notifyuser)
+                       {
+                               if (msgtarget.type == MessageTarget::TYPE_CHANNEL)
+                                       user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<Channel>(), InspIRCd::Format("Your message to this channel was blocked: %s.", f->reason.c_str())));
+                               else
+                                       user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<User>(), InspIRCd::Format("Your message to this user was blocked: %s.", f->reason.c_str())));
+                       }
+                       else
+                               details.echo_original = true;
+               }
+               else if (f->action == FA_SILENT)
+               {
+                       if (notifyuser)
+                       {
+                               if (msgtarget.type == MessageTarget::TYPE_CHANNEL)
+                                       user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<Channel>(), InspIRCd::Format("Your message to this channel was blocked: %s.", f->reason.c_str())));
+                               else
+                                       user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<User>(), InspIRCd::Format("Your message to this user was blocked: %s.", f->reason.c_str())));
+                       }
+                       else
+                               details.echo_original = true;
+               }
+               else if (f->action == FA_KILL)
+               {
+                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s was killed because their message to %s matched %s (%s)",
+                               user->nick.c_str(), msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
+                       ServerInstance->Users->QuitUser(user, "Filtered: " + f->reason);
+               }
+               else if (f->action == FA_SHUN && (ServerInstance->XLines->GetFactory("SHUN")))
+               {
+                       Shun* sh = new Shun(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), user->GetIPString());
+                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was shunned for %s (expires on %s) because their message to %s matched %s (%s)",
+                               user->nick.c_str(), sh->Displayable().c_str(), InspIRCd::DurationString(f->duration).c_str(),
+                               InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
+                               msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
+                       if (ServerInstance->XLines->AddLine(sh, NULL))
+                       {
+                               ServerInstance->XLines->ApplyLines();
+                       }
+                       else
+                               delete sh;
+               }
+               else if (f->action == FA_GLINE)
+               {
+                       GLine* gl = new GLine(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), "*", user->GetIPString());
+                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was G-lined for %s (expires on %s) because their message to %s matched %s (%s)",
+                               user->nick.c_str(), gl->Displayable().c_str(), InspIRCd::DurationString(f->duration).c_str(),
+                               InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
+                               msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
+                       if (ServerInstance->XLines->AddLine(gl,NULL))
+                       {
+                               ServerInstance->XLines->ApplyLines();
+                       }
+                       else
+                               delete gl;
+               }
+               else if (f->action == FA_ZLINE)
+               {
+                       ZLine* zl = new ZLine(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), user->GetIPString());
+                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was Z-lined for %s (expires on %s) because their message to %s matched %s (%s)",
+                               user->nick.c_str(), zl->Displayable().c_str(), InspIRCd::DurationString(f->duration).c_str(),
+                               InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
+                               msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
+                       if (ServerInstance->XLines->AddLine(zl,NULL))
+                       {
+                               ServerInstance->XLines->ApplyLines();
+                       }
+                       else
+                               delete zl;
+               }
+
+               ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, user->nick + " had their message filtered, target was " + msgtarget.GetName() + ": " + f->reason + " Action: " + ModuleFilter::FilterActionToString(f->action));
+               return MOD_RES_DENY;
        }
+       return MOD_RES_PASSTHRU;
+}
 
-       virtual FilterResult* FilterMatch(User* user, const std::string &text, int iflags)
+ModResult ModuleFilter::OnPreCommand(std::string& command, CommandBase::Params& parameters, LocalUser* user, bool validated)
+{
+       if (validated)
        {
-               for (filter_t::iterator index = filters.begin(); index != filters.end(); index++)
+               flags = 0;
+               bool parting;
+
+               if (command == "QUIT")
+               {
+                       /* QUIT with no reason: nothing to do */
+                       if (parameters.size() < 1)
+                               return MOD_RES_PASSTHRU;
+
+                       parting = false;
+                       flags = FLAG_QUIT;
+               }
+               else if (command == "PART")
                {
+                       /* PART with no reason: nothing to do */
+                       if (parameters.size() < 2)
+                               return MOD_RES_PASSTHRU;
+
+                       if (exemptedchans.count(parameters[0]))
+                               return MOD_RES_PASSTHRU;
+
+                       parting = true;
+                       flags = FLAG_PART;
+               }
+               else
+                       /* We're only messing with PART and QUIT */
+                       return MOD_RES_PASSTHRU;
+
+               FilterResult* f = this->FilterMatch(user, parameters[parting ? 1 : 0], flags);
+               if (!f)
+                       /* PART or QUIT reason doesnt match a filter */
+                       return MOD_RES_PASSTHRU;
 
-                       /* Skip ones that dont apply to us */
-                       if (!FilterBase::AppliesToMe(user, index->second, iflags))
-                               continue;
+               /* We cant block a part or quit, so instead we change the reason to 'Reason filtered' */
+               parameters[parting ? 1 : 0] = "Reason filtered";
+
+               /* We're warning or blocking, OR they're quitting and its a KILL action
+                * (we cant kill someone whos already quitting, so filter them anyway)
+                */
+               if ((f->action == FA_WARN) || (f->action == FA_BLOCK) || (((!parting) && (f->action == FA_KILL))) || (f->action == FA_SILENT))
+               {
+                       return MOD_RES_PASSTHRU;
+               }
+               else
+               {
+                       /* Are they parting, if so, kill is applicable */
+                       if ((parting) && (f->action == FA_KILL))
+                       {
+                               user->WriteNotice("*** Your PART message was filtered: " + f->reason);
+                               ServerInstance->Users->QuitUser(user, "Filtered: " + f->reason);
+                       }
+                       if (f->action == FA_GLINE)
+                       {
+                               /* Note: We G-line *@IP so that if their host doesn't resolve the G-line still applies. */
+                               GLine* gl = new GLine(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), "*", user->GetIPString());
+                               ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was G-lined for %s (expires on %s) because their %s message matched %s (%s)",
+                                       user->nick.c_str(), gl->Displayable().c_str(),
+                                       InspIRCd::DurationString(f->duration).c_str(),
+                                       InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
+                                       command.c_str(), f->freeform.c_str(), f->reason.c_str()));
 
-                       if (ServerInstance->MatchText(text,index->first))
+                               if (ServerInstance->XLines->AddLine(gl,NULL))
+                               {
+                                       ServerInstance->XLines->ApplyLines();
+                               }
+                               else
+                                       delete gl;
+                       }
+                       if (f->action == FA_ZLINE)
                        {
-                               FilterResult* fr = index->second;
-                               if (index != filters.begin())
+                               ZLine* zl = new ZLine(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), user->GetIPString());
+                               ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was Z-lined for %s (expires on %s) because their %s message matched %s (%s)",
+                                       user->nick.c_str(), zl->Displayable().c_str(),
+                                       InspIRCd::DurationString(f->duration).c_str(),
+                                       InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
+                                       command.c_str(), f->freeform.c_str(), f->reason.c_str()));
+
+                               if (ServerInstance->XLines->AddLine(zl,NULL))
                                {
-                                       std::string pat = index->first;
-                                       filters.erase(index);
-                                       filters.insert(filters.begin(), std::make_pair(pat,fr));
+                                       ServerInstance->XLines->ApplyLines();
                                }
-                               return fr;
+                               else
+                                       delete zl;
                        }
+                       else if (f->action == FA_SHUN && (ServerInstance->XLines->GetFactory("SHUN")))
+                       {
+                               /* Note: We shun *!*@IP so that if their host doesnt resolve the shun still applies. */
+                               Shun* sh = new Shun(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), user->GetIPString());
+                               ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was shunned for %s (expires on %s) because their %s message matched %s (%s)",
+                                       user->nick.c_str(), sh->Displayable().c_str(),
+                                       InspIRCd::DurationString(f->duration).c_str(),
+                                       InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
+                                       command.c_str(), f->freeform.c_str(), f->reason.c_str()));
+
+                               if (ServerInstance->XLines->AddLine(sh, NULL))
+                               {
+                                       ServerInstance->XLines->ApplyLines();
+                               }
+                               else
+                                       delete sh;
+                       }
+                       return MOD_RES_DENY;
                }
-               return NULL;
        }
+       return MOD_RES_PASSTHRU;
+}
+
+void ModuleFilter::ReadConfig(ConfigStatus& status)
+{
+       ConfigTagList tags = ServerInstance->Config->ConfTags("exemptfromfilter");
+       exemptedchans.clear();
+       exemptednicks.clear();
 
-       virtual bool DeleteFilter(const std::string &freeform)
+       for (ConfigIter i = tags.first; i != tags.second; ++i)
        {
-               if (filters.find(freeform) != filters.end())
+               ConfigTag* tag = i->second;
+
+               // If "target" is not found, try the old "channel" key to keep compatibility with 2.0 configs
+               const std::string target = tag->getString("target", tag->getString("channel"), 1);
+               if (!target.empty())
                {
-                       delete (filters.find(freeform))->second;
-                       filters.erase(filters.find(freeform));
+                       if (target[0] == '#')
+                               exemptedchans.insert(target);
+                       else
+                               exemptednicks.insert(target);
+               }
+       }
+
+       ConfigTag* tag = ServerInstance->Config->ConfValue("filteropts");
+       std::string newrxengine = tag->getString("engine");
+       notifyuser = tag->getBool("notifyuser", true);
+       warnonselfmsg = tag->getBool("warnonselfmsg");
+
+       factory = RegexEngine ? (RegexEngine.operator->()) : NULL;
+
+       if (newrxengine.empty())
+               RegexEngine.SetProvider("regex");
+       else
+               RegexEngine.SetProvider("regex/" + newrxengine);
+
+       if (!RegexEngine)
+       {
+               if (newrxengine.empty())
+                       ServerInstance->SNO->WriteGlobalSno('f', "WARNING: No regex engine loaded - Filter functionality disabled until this is corrected.");
+               else
+                       ServerInstance->SNO->WriteGlobalSno('f', "WARNING: Regex engine '%s' is not loaded - Filter functionality disabled until this is corrected.", newrxengine.c_str());
+
+               initing = false;
+               FreeFilters();
+               return;
+       }
+
+       if ((!initing) && (RegexEngine.operator->() != factory))
+       {
+               ServerInstance->SNO->WriteGlobalSno('f', "Dumping all filters due to regex engine change");
+               FreeFilters();
+       }
+
+       initing = false;
+       ReadFilters();
+}
+
+Version ModuleFilter::GetVersion()
+{
+       return Version("Adds the /FILTER command which allows server operators to define regex matches for inappropriate phrases that are not allowed to be used in channel messages, private messages, part messages, or quit messages.", VF_VENDOR | VF_COMMON, RegexEngine ? RegexEngine->name : "");
+}
+
+std::string ModuleFilter::EncodeFilter(FilterResult* filter)
+{
+       std::ostringstream stream;
+       std::string x = filter->freeform;
+
+       /* Hax to allow spaces in the freeform without changing the design of the irc protocol */
+       for (std::string::iterator n = x.begin(); n != x.end(); n++)
+               if (*n == ' ')
+                       *n = '\7';
+
+       stream << x << " " << FilterActionToString(filter->action) << " " << filter->GetFlags() << " " << filter->duration << " :" << filter->reason;
+       return stream.str();
+}
+
+FilterResult ModuleFilter::DecodeFilter(const std::string &data)
+{
+       std::string filteraction;
+       FilterResult res;
+       irc::tokenstream tokens(data);
+       tokens.GetMiddle(res.freeform);
+       tokens.GetMiddle(filteraction);
+       if (!StringToFilterAction(filteraction, res.action))
+               throw ModuleException("Invalid action: " + filteraction);
+
+       std::string filterflags;
+       tokens.GetMiddle(filterflags);
+       char c = res.FillFlags(filterflags);
+       if (c != 0)
+               throw ModuleException("Invalid flag: '" + std::string(1, c) + "'");
+
+       std::string duration;
+       tokens.GetMiddle(duration);
+       res.duration = ConvToNum<unsigned long>(duration);
+
+       tokens.GetTrailing(res.reason);
+
+       /* Hax to allow spaces in the freeform without changing the design of the irc protocol */
+       for (std::string::iterator n = res.freeform.begin(); n != res.freeform.end(); n++)
+               if (*n == '\7')
+                       *n = ' ';
+
+       return res;
+}
+
+void ModuleFilter::OnSyncNetwork(ProtocolInterface::Server& server)
+{
+       for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); ++i)
+       {
+               FilterResult& filter = *i;
+               if (filter.from_config)
+                       continue;
+
+               server.SendMetaData("filter", EncodeFilter(&filter));
+       }
+}
+
+void ModuleFilter::OnDecodeMetaData(Extensible* target, const std::string &extname, const std::string &extdata)
+{
+       if ((target == NULL) && (extname == "filter"))
+       {
+               try
+               {
+                       FilterResult data = DecodeFilter(extdata);
+                       this->AddFilter(data.freeform, data.action, data.reason, data.duration, data.GetFlags());
+               }
+               catch (ModuleException& e)
+               {
+                       ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "Error when unserializing filter: " + e.GetReason());
+               }
+       }
+}
+
+FilterResult* ModuleFilter::FilterMatch(User* user, const std::string &text, int flgs)
+{
+       static std::string stripped_text;
+       stripped_text.clear();
+
+       for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); ++i)
+       {
+               FilterResult* filter = &*i;
+
+               /* Skip ones that dont apply to us */
+               if (!AppliesToMe(user, filter, flgs))
+                       continue;
+
+               if ((filter->flag_strip_color) && (stripped_text.empty()))
+               {
+                       stripped_text = text;
+                       InspIRCd::StripColor(stripped_text);
+               }
+
+               if (filter->regex->Matches(filter->flag_strip_color ? stripped_text : text))
+                       return filter;
+       }
+       return NULL;
+}
+
+bool ModuleFilter::DeleteFilter(const std::string& freeform, std::string& reason)
+{
+       for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); i++)
+       {
+               if (i->freeform == freeform)
+               {
+                       reason.assign(i->reason);
+                       delete i->regex;
+                       filters.erase(i);
                        return true;
                }
-               return false;
        }
+       return false;
+}
 
-       virtual std::pair<bool, std::string> AddFilter(const std::string &freeform, const std::string &type, const std::string &reason, long duration, const std::string &sflags)
+std::pair<bool, std::string> ModuleFilter::AddFilter(const std::string& freeform, FilterAction type, const std::string& reason, unsigned long duration, const std::string& flgs, bool config)
+{
+       for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); i++)
        {
-               if (filters.find(freeform) != filters.end())
+               if (i->freeform == freeform)
                {
                        return std::make_pair(false, "Filter already exists");
                }
+       }
+
+       try
+       {
+               filters.push_back(FilterResult(RegexEngine, freeform, reason, type, duration, flgs, config));
+       }
+       catch (ModuleException &e)
+       {
+               ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Error in regular expression '%s': %s", freeform.c_str(), e.GetReason().c_str());
+               return std::make_pair(false, e.GetReason());
+       }
+       return std::make_pair(true, "");
+}
 
-               FilterResult* x = new FilterResult(freeform, reason, type, duration, sflags);
-               filters[freeform] = x;
+bool ModuleFilter::StringToFilterAction(const std::string& str, FilterAction& fa)
+{
+       if (stdalgo::string::equalsci(str, "gline"))
+               fa = FA_GLINE;
+       else if (stdalgo::string::equalsci(str, "zline"))
+               fa = FA_ZLINE;
+       else if (stdalgo::string::equalsci(str, "warn"))
+               fa = FA_WARN;
+       else if (stdalgo::string::equalsci(str, "block"))
+               fa = FA_BLOCK;
+       else if (stdalgo::string::equalsci(str, "silent"))
+               fa = FA_SILENT;
+       else if (stdalgo::string::equalsci(str, "kill"))
+               fa = FA_KILL;
+       else if (stdalgo::string::equalsci(str, "shun") && (ServerInstance->XLines->GetFactory("SHUN")))
+               fa = FA_SHUN;
+       else if (stdalgo::string::equalsci(str, "none"))
+               fa = FA_NONE;
+       else
+               return false;
 
-               return std::make_pair(true, "");
+       return true;
+}
+
+std::string ModuleFilter::FilterActionToString(FilterAction fa)
+{
+       switch (fa)
+       {
+               case FA_GLINE:  return "gline";
+               case FA_ZLINE:  return "zline";
+               case FA_WARN:   return "warn";
+               case FA_BLOCK:  return "block";
+               case FA_SILENT: return "silent";
+               case FA_KILL:   return "kill";
+               case FA_SHUN:   return "shun";
+               default:                return "none";
        }
+}
+
+void ModuleFilter::ReadFilters()
+{
+       insp::flat_set<std::string> removedfilters;
 
-       virtual void SyncFilters(Module* proto, void* opaque)
+       for (std::vector<FilterResult>::iterator filter = filters.begin(); filter != filters.end(); )
        {
-               for (filter_t::iterator n = filters.begin(); n != filters.end(); n++)
+               if (filter->from_config)
                {
-                       this->SendFilter(proto, opaque, n->second);
+                       removedfilters.insert(filter->freeform);
+                       delete filter->regex;
+                       filter = filters.erase(filter);
+                       continue;
                }
+
+               // The filter is not from the config.
+               filter++;
        }
 
-       virtual void OnRehash(User* user, const std::string &parameter)
+       ConfigTagList tags = ServerInstance->Config->ConfTags("keyword");
+       for (ConfigIter i = tags.first; i != tags.second; ++i)
        {
-               ConfigReader* MyConf = new ConfigReader(ServerInstance);
+               std::string pattern = i->second->getString("pattern");
+               std::string reason = i->second->getString("reason");
+               std::string action = i->second->getString("action");
+               std::string flgs = i->second->getString("flags");
+               unsigned long duration = i->second->getDuration("duration", 10*60, 1);
+               if (flgs.empty())
+                       flgs = "*";
 
-               for (int index = 0; index < MyConf->Enumerate("keyword"); index++)
-               {
-                       this->DeleteFilter(MyConf->ReadValue("keyword","pattern",index));
+               FilterAction fa;
+               if (!StringToFilterAction(action, fa))
+                       fa = FA_NONE;
 
-                       std::string pattern = MyConf->ReadValue("keyword","pattern",index);
-                       std::string reason = MyConf->ReadValue("keyword","reason",index);
-                       std::string do_action = MyConf->ReadValue("keyword","action",index);
-                       std::string sflags = MyConf->ReadValue("keyword","flags",index);
-                       long gline_time = ServerInstance->Duration(MyConf->ReadValue("keyword","duration",index));
-                       if (do_action.empty())
-                               do_action = "none";
-                       if (sflags.empty())
-                               sflags = "*";
-                       FilterResult* x = new FilterResult(pattern, reason, do_action, gline_time, sflags);
-                       filters[pattern] = x;
-               }
-               delete MyConf;
-               FilterBase::OnRehash(user, parameter);
+               std::pair<bool, std::string> result = static_cast<ModuleFilter*>(this)->AddFilter(pattern, fa, reason, duration, flgs, true);
+               if (result.first)
+                       removedfilters.erase(pattern);
+               else
+                       ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Filter '%s' could not be added: %s", pattern.c_str(), result.second.c_str());
        }
 
-       virtual int OnStats(char symbol, User* user, string_list &results)
+       if (!removedfilters.empty())
        {
-               if (symbol == 's')
+               for (insp::flat_set<std::string>::const_iterator it = removedfilters.begin(); it != removedfilters.end(); ++it)
+                       ServerInstance->SNO->WriteGlobalSno('f', "Removing filter '" + *(it) + "' due to config rehash.");
+       }
+}
+
+ModResult ModuleFilter::OnStats(Stats::Context& stats)
+{
+       if (stats.GetSymbol() == 's')
+       {
+               for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); i++)
                {
-                       std::string sn = ServerInstance->Config->ServerName;
-                       for (filter_t::iterator n = filters.begin(); n != filters.end(); n++)
-                       {
-                               results.push_back(sn+" 223 "+user->nick+" :GLOB:"+n->second->freeform+" "+n->second->flags+" "+n->second->action+" "+ConvToStr(n->second->gline_time)+" :"+n->second->reason);
-                       }
+                       stats.AddRow(223, RegexEngine.GetProvider(), i->freeform, i->GetFlags(), FilterActionToString(i->action), i->duration, i->reason);
+               }
+               for (ExemptTargetSet::const_iterator i = exemptedchans.begin(); i != exemptedchans.end(); ++i)
+               {
+                       stats.AddRow(223, "EXEMPT "+(*i));
+               }
+               for (ExemptTargetSet::const_iterator i = exemptednicks.begin(); i != exemptednicks.end(); ++i)
+               {
+                       stats.AddRow(223, "EXEMPT "+(*i));
                }
-               return 0;
        }
-};
+       return MOD_RES_PASSTHRU;
+}
 
+void ModuleFilter::OnUnloadModule(Module* mod)
+{
+       // If the regex engine became unavailable or has changed, remove all filters
+       if (!RegexEngine)
+       {
+               FreeFilters();
+       }
+       else if (RegexEngine.operator->() != factory)
+       {
+               factory = RegexEngine.operator->();
+               FreeFilters();
+       }
+}
 
 MODULE_INIT(ModuleFilter)