summaryrefslogtreecommitdiff
path: root/modules/protocol/inspircd.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'modules/protocol/inspircd.cpp')
-rw-r--r--modules/protocol/inspircd.cpp2168
1 files changed, 2168 insertions, 0 deletions
diff --git a/modules/protocol/inspircd.cpp b/modules/protocol/inspircd.cpp
new file mode 100644
index 000000000..275f963a6
--- /dev/null
+++ b/modules/protocol/inspircd.cpp
@@ -0,0 +1,2168 @@
+/* InspIRCd functions
+ *
+ * (C) 2003-2024 Anope Team
+ * Contact us at team@anope.org
+ *
+ * Please read COPYING and README for further details.
+ *
+ * Based on the original code of Epona by Lara.
+ * Based on the original code of Services by Andy Church.
+ */
+
+#include "module.h"
+#include "modules/cs_mode.h"
+#include "modules/sasl.h"
+
+typedef std::map<char, unsigned> ListLimits;
+
+struct SASLUser final
+{
+ Anope::string uid;
+ Anope::string acc;
+ time_t created;
+};
+
+namespace
+{
+ // The maximum length of a channel name.
+ size_t maxchannel = 0;
+
+ // The maximum length of a hostname.
+ size_t maxhost = 0;
+
+ // The maximum length of a nickname.
+ size_t maxnick = 0;
+
+ // The maximum length of a username.
+ size_t maxuser = 0;
+
+ // The SID of a server we are waiting to squit.
+ Anope::string rsquit_id;
+
+ // The hostname of a server we are waiting to squit.
+ Anope::string rsquit_server;
+
+ // Non-introduced users who have authenticated via SASL.
+ std::list<SASLUser> saslusers;
+
+ // Parses a module name in the format "m_foo.so=bar" to {foo, bar}.
+ void ParseModule(const Anope::string &module, Anope::string &modname, Anope::string &moddata)
+ {
+ size_t sep = module.find('=');
+
+ // Extract and clean up the module name.
+ modname = module.substr(0, sep);
+ if (modname.compare(0, 2, "m_", 2) == 0)
+ modname.erase(0, 2);
+
+ if (modname.length() > 3 && modname.compare(modname.length() - 3, 3, ".so", 3) == 0)
+ modname.erase(modname.length() - 3);
+
+ // Extract the module link data (if any).
+ moddata = (sep == Anope::string::npos) ? "" : module.substr(sep);
+
+ Log(LOG_DEBUG) << "Parsed module: " << "name=" << modname << " data=" << moddata;
+ }
+}
+
+class InspIRCdProto final
+ : public IRCDProto
+{
+private:
+ static void SendChgIdentInternal(const Anope::string &nick, const Anope::string &vIdent)
+ {
+ if (!Servers::Capab.count("CHGIDENT"))
+ Log() << "CHGIDENT not loaded!";
+ else
+ Uplink::Send("CHGIDENT", nick, vIdent);
+ }
+
+ static void SendChgHostInternal(const Anope::string &nick, const Anope::string &vhost)
+ {
+ if (!Servers::Capab.count("CHGHOST"))
+ Log() << "CHGHOST not loaded!";
+ else
+ Uplink::Send("CHGHOST", nick, vhost);
+ }
+
+ static void SendAddLine(const Anope::string &xtype, const Anope::string &mask, time_t duration, const Anope::string &addedby, const Anope::string &reason)
+ {
+ Uplink::Send("ADDLINE", xtype, mask, addedby, Anope::CurTime, duration, reason);
+ }
+
+ static void SendDelLine(const Anope::string &xtype, const Anope::string &mask)
+ {
+ Uplink::Send("DELLINE", xtype, mask);
+ }
+
+ static void SendAccount(const Anope::string &uid, NickAlias *na)
+ {
+ Uplink::Send("METADATA", uid, "accountid", na ? na->nc->GetId() : Anope::string());
+ Uplink::Send("METADATA", uid, "accountname", na ? na->nc->display : Anope::string());
+ }
+
+public:
+ PrimitiveExtensibleItem<ListLimits> maxlist;
+
+ InspIRCdProto(Module *creator) : IRCDProto(creator, "InspIRCd 3+"), maxlist(creator, "maxlist")
+ {
+ DefaultPseudoclientModes = "+oI";
+ CanSVSNick = true;
+ CanSVSJoin = true;
+ CanSetVHost = true;
+ CanSetVIdent = true;
+ CanSQLine = true;
+ CanSQLineChannel = true;
+ CanSZLine = true;
+ CanSVSHold = true;
+ CanSVSLogout = true;
+ CanCertFP = true;
+ RequiresID = true;
+ MaxModes = 20;
+ MaxLine = 4096;
+ }
+
+ size_t GetMaxChannel() override
+ {
+ return maxchannel ? maxchannel : IRCDProto::GetMaxChannel();
+ }
+
+ size_t GetMaxHost() override
+ {
+ return maxhost ? maxhost : IRCDProto::GetMaxHost();
+ }
+
+ size_t GetMaxListFor(Channel *c, ChannelMode *cm) override
+ {
+ ListLimits *limits = maxlist.Get(c);
+ if (limits)
+ {
+ ListLimits::const_iterator limit = limits->find(cm->mchar);
+ if (limit != limits->end())
+ return limit->second;
+ }
+
+ // Fall back to the config limit if we can't find the mode.
+ return IRCDProto::GetMaxListFor(c, cm);
+ }
+
+ size_t GetMaxNick() override
+ {
+ return maxnick ? maxnick : IRCDProto::GetMaxNick();
+ }
+
+ size_t GetMaxUser() override
+ {
+ return maxuser ? maxuser : IRCDProto::GetMaxUser();
+ }
+
+ void SendConnect() override
+ {
+ Uplink::Send("CAPAB", "START", 1205);
+ Uplink::Send("CAPAB", "CAPABILITIES", "CASEMAPPING=" + Config->GetBlock("options")->Get<const Anope::string>("casemap", "ascii"));
+ Uplink::Send("CAPAB", "END");
+ Uplink::Send("SERVER", Me->GetName(), Config->Uplinks[Anope::CurrentUplink].password, 0, Me->GetSID(), Me->GetDescription());
+ }
+
+ void SendSASLMechanisms(std::vector<Anope::string> &mechanisms) override
+ {
+ Anope::string mechlist;
+ for (const auto &mechanism : mechanisms)
+ mechlist += "," + mechanism;
+
+ Uplink::Send("METADATA", "*", "saslmechlist", mechanisms.empty() ? "" : mechlist.substr(1));
+ }
+
+ void SendSVSKill(const MessageSource &source, User *user, const Anope::string &buf) override
+ {
+ IRCDProto::SendSVSKill(source, user, buf);
+ user->KillInternal(source, buf);
+ }
+
+ void SendForceNickChange(User *u, const Anope::string &newnick, time_t when) override
+ {
+ Uplink::Send("SVSNICK", u->GetUID(), newnick, when, u->timestamp);
+ }
+
+ void SendGlobalNotice(BotInfo *bi, const Server *dest, const Anope::string &msg) override
+ {
+ Uplink::Send(bi, "NOTICE", "$" + dest->GetName(), msg);
+ }
+
+ void SendGlobalPrivmsg(BotInfo *bi, const Server *dest, const Anope::string &msg) override
+ {
+ Uplink::Send(bi, "PRIVMSG", "$" + dest->GetName(), msg);
+ }
+
+ void SendPong(const Anope::string &servname, const Anope::string &who) override
+ {
+ Server *serv = servname.empty() ? NULL : Server::Find(servname);
+ if (!serv)
+ serv = Me;
+
+ Uplink::Send(serv, "PONG", who);
+ }
+
+ void SendAkillDel(const XLine *x) override
+ {
+ {
+ /* InspIRCd may support regex bans
+ * Mask is expected in format: 'n!u@h\sr' and spaces as '\s'
+ * We remove the '//' and replace '#' and any ' ' with '\s'
+ */
+ if (x->IsRegex() && Servers::Capab.count("RLINE"))
+ {
+ Anope::string mask = x->mask;
+ if (mask.length() >= 2 && mask[0] == '/' && mask[mask.length() - 1] == '/')
+ mask = mask.substr(1, mask.length() - 2);
+ size_t h = mask.find('#');
+ if (h != Anope::string::npos)
+ {
+ mask = mask.replace(h, 1, "\\s");
+ mask = mask.replace_all_cs(" ", "\\s");
+ }
+ SendDelLine("R", mask);
+ return;
+ }
+ else if (x->IsRegex() || x->HasNickOrReal())
+ return;
+
+ /* ZLine if we can instead */
+ if (x->GetUser() == "*")
+ {
+ cidr addr(x->GetHost());
+ if (addr.valid())
+ {
+ IRCD->SendSZLineDel(x);
+ return;
+ }
+ }
+
+ SendDelLine("G", x->GetUser() + "@" + x->GetHost());
+ }
+ }
+
+ void SendInvite(const MessageSource &source, const Channel *c, User *u) override
+ {
+ Uplink::Send(source, "INVITE", u->GetUID(), c->name, c->creation_time);
+ }
+
+ void SendTopic(const MessageSource &source, Channel *c) override
+ {
+ if (Servers::Capab.count("SVSTOPIC"))
+ {
+ Uplink::Send(c->WhoSends(), "SVSTOPIC", c->name, c->topic_ts, c->topic_setter, c->topic);
+ }
+ else
+ {
+ /* If the last time a topic was set is after the TS we want for this topic we must bump this topic's timestamp to now */
+ time_t ts = c->topic_ts;
+ if (c->topic_time > ts)
+ ts = Anope::CurTime;
+ /* But don't modify c->topic_ts, it should remain set to the real TS we want as ci->last_topic_time pulls from it */
+ Uplink::Send(source, "FTOPIC", c->name, c->creation_time, ts, c->topic_setter, c->topic);
+ }
+ }
+
+ void SendVhostDel(User *u) override
+ {
+ UserMode *um = ModeManager::FindUserModeByName("CLOAK");
+
+ if (um && !u->HasMode(um->name))
+ // Just set +x if we can
+ u->SetMode(NULL, um);
+ else
+ // Try to restore cloaked host
+ this->SendChgHostInternal(u->nick, u->chost);
+ }
+
+ void SendAkill(User *u, XLine *x) override
+ {
+ // Calculate the time left before this would expire
+ time_t timeleft = x->expires ? x->expires - Anope::CurTime : x->expires;
+
+ /* InspIRCd may support regex bans, if they do we can send this and forget about it
+ * Mask is expected in format: 'n!u@h\sr' and spaces as '\s'
+ * We remove the '//' and replace '#' and any ' ' with '\s'
+ */
+ if (x->IsRegex() && Servers::Capab.count("RLINE"))
+ {
+ Anope::string mask = x->mask;
+ if (mask.length() >= 2 && mask[0] == '/' && mask[mask.length() - 1] == '/')
+ mask = mask.substr(1, mask.length() - 2);
+ size_t h = mask.find('#');
+ if (h != Anope::string::npos)
+ {
+ mask = mask.replace(h, 1, "\\s");
+ mask = mask.replace_all_cs(" ", "\\s");
+ }
+ SendAddLine("R", mask, timeleft, x->by, x->GetReason());
+ return;
+ }
+ else if (x->IsRegex() || x->HasNickOrReal())
+ {
+ if (!u)
+ {
+ /* No user (this akill was just added), and contains nick and/or realname. Find users that match and ban them */
+ for (const auto &[_, user] : UserListByNick)
+ if (x->manager->Check(user, x))
+ this->SendAkill(user, x);
+ return;
+ }
+
+ const XLine *old = x;
+
+ if (old->manager->HasEntry("*@" + u->host))
+ return;
+
+ /* We can't akill x as it has a nick and/or realname included, so create a new akill for *@host */
+ x = new XLine("*@" + u->host, old->by, old->expires, old->reason, old->id);
+ old->manager->AddXLine(x);
+
+ Log(Config->GetClient("OperServ"), "akill") << "AKILL: Added an akill for " << x->mask << " because " << u->GetMask() << "#" << u->realname << " matches " << old->mask;
+ }
+
+ /* ZLine if we can instead */
+ if (x->GetUser() == "*")
+ {
+ cidr addr(x->GetHost());
+ if (addr.valid())
+ {
+ IRCD->SendSZLine(u, x);
+ return;
+ }
+ }
+
+ SendAddLine("G", x->GetUser() + "@" + x->GetHost(), timeleft, x->by, x->GetReason());
+ }
+
+ void SendNumericInternal(int numeric, const Anope::string &dest, const std::vector<Anope::string> &params) override
+ {
+ auto newparams = params;
+ newparams.insert(newparams.begin(), { Me->GetSID(), dest, stringify(numeric) });
+ Uplink::SendInternal({}, Me, "NUM", newparams);
+ }
+
+ void SendModeInternal(const MessageSource &source, Channel *chan, const Anope::string &modes, const std::vector<Anope::string> &values) override
+ {
+ auto params = values;
+ params.insert(params.begin(), { chan->name, stringify(chan->creation_time), modes });
+ Uplink::SendInternal({}, source, "FMODE", params);
+ }
+
+ void SendClientIntroduction(User *u) override
+ {
+ Uplink::Send("UID", u->GetUID(), u->timestamp, u->nick, u->host, u->host, u->GetIdent(), "0.0.0.0", u->timestamp, "+" + u->GetModes(), u->realname);
+
+ if (u->GetModes().find('o') != Anope::string::npos)
+ {
+ // Mark as introduced so we can send an oper type.
+ BotInfo *bi = BotInfo::Find(u->nick, true);
+ if (bi)
+ bi->introduced = true;
+
+ Uplink::Send(u, "OPERTYPE", "service");
+ }
+ }
+
+ void SendServer(const Server *server) override
+ {
+ /* if rsquit is set then we are waiting on a squit */
+ if (rsquit_id.empty() && rsquit_server.empty())
+ Uplink::Send("SERVER", server->GetName(), server->GetSID(), server->GetDescription());
+ }
+
+ void SendSquit(Server *s, const Anope::string &message) override
+ {
+ if (s != Me)
+ {
+ rsquit_id = s->GetSID();
+ rsquit_server = s->GetName();
+ Uplink::Send("RSQUIT", s->GetName(), message);
+ }
+ else
+ Uplink::Send("SQUIT", s->GetName(), message);
+ }
+
+ void SendJoin(User *user, Channel *c, const ChannelStatus *status) override
+ {
+ Uplink::Send("FJOIN", c->name, c->creation_time, "+" + c->GetModes(true, true), "," + user->GetUID());
+ /* Note that we can send this with the FJOIN but choose not to
+ * because the mode stacker will handle this and probably will
+ * merge these modes with +nrt and other mlocked modes
+ */
+ if (status)
+ {
+ /* First save the channel status incase uc->Status == status */
+ ChannelStatus cs = *status;
+ /* If the user is internally on the channel with flags, kill them so that
+ * the stacker will allow this.
+ */
+ ChanUserContainer *uc = c->FindUser(user);
+ if (uc != NULL)
+ uc->status.Clear();
+
+ BotInfo *setter = BotInfo::Find(user->GetUID());
+ for (auto mode : cs.Modes())
+ c->SetMode(setter, ModeManager::FindChannelModeByChar(mode), user->GetUID(), false);
+
+ if (uc != NULL)
+ uc->status = cs;
+ }
+ }
+
+ void SendSQLineDel(const XLine *x) override
+ {
+ if (IRCD->CanSQLineChannel && (x->mask[0] == '#'))
+ SendDelLine("CBAN", x->mask);
+ else
+ SendDelLine("Q", x->mask);
+ }
+
+ void SendSQLine(User *u, const XLine *x) override
+ {
+ // Calculate the time left before this would expire
+ time_t timeleft = x->expires ? x->expires - Anope::CurTime : x->expires;
+
+ if (IRCD->CanSQLineChannel && (x->mask[0] == '#'))
+ SendAddLine("CBAN", x->mask, timeleft, x->by, x->GetReason());
+ else
+ SendAddLine("Q", x->mask, timeleft, x->by, x->GetReason());
+ }
+
+ void SendVhost(User *u, const Anope::string &vIdent, const Anope::string &vhost) override
+ {
+ if (!vIdent.empty())
+ this->SendChgIdentInternal(u->nick, vIdent);
+ if (!vhost.empty())
+ this->SendChgHostInternal(u->nick, vhost);
+ }
+
+ void SendSVSHold(const Anope::string &nick, time_t t) override
+ {
+ Uplink::Send(Config->GetClient("NickServ"), "SVSHOLD", nick, t, "Being held for a registered user");
+ }
+
+ void SendSVSHoldDel(const Anope::string &nick) override
+ {
+ Uplink::Send(Config->GetClient("NickServ"), "SVSHOLD", nick);
+ }
+
+ void SendSZLineDel(const XLine *x) override
+ {
+ SendDelLine("Z", x->GetHost());
+ }
+
+ void SendSZLine(User *u, const XLine *x) override
+ {
+ // Calculate the time left before this would expire
+ time_t timeleft = x->expires ? x->expires - Anope::CurTime : x->expires;
+
+ SendAddLine("Z", x->GetHost(), timeleft, x->by, x->GetReason());
+ }
+
+ void SendSVSJoin(const MessageSource &source, User *u, const Anope::string &chan, const Anope::string &other) override
+ {
+ Uplink::Send(source, "SVSJOIN", u->GetUID(), chan);
+ }
+
+ void SendSVSPart(const MessageSource &source, User *u, const Anope::string &chan, const Anope::string &param) override
+ {
+ if (!param.empty())
+ Uplink::Send(source, "SVSPART", u->GetUID(), chan, param);
+ else
+ Uplink::Send(source, "SVSPART", u->GetUID(), chan);
+ }
+
+ void SendSWhois(const MessageSource &bi, const Anope::string &who, const Anope::string &mask) override
+ {
+ User *u = User::Find(who);
+ Uplink::Send("METADATA", u->GetUID(), "swhois", mask);
+ }
+
+ void SendBOB() override
+ {
+ Uplink::Send("BURST", Anope::CurTime);
+ Module *enc = ModuleManager::FindFirstOf(ENCRYPTION);
+
+ Uplink::Send("SINFO", "version", Anope::printf("Anope-%s %s :%s -- (%s) -- %s", Anope::Version().c_str(), Me->GetName().c_str(), IRCD->GetProtocolName().c_str(), enc ? enc->name.c_str() : "none", Anope::VersionBuildString().c_str()));
+ Uplink::Send("SINFO", "fullversion", Anope::printf("Anope-%s %s :[%s] %s -- (%s) -- %s", Anope::Version().c_str(), Me->GetName().c_str(), Me->GetSID().c_str(), IRCD->GetProtocolName().c_str(), enc ? enc->name.c_str() : "none", Anope::VersionBuildString().c_str()));
+ Uplink::Send("SINFO", "rawversion", "Anope-" + Anope::VersionShort());
+ }
+
+ void SendEOB() override
+ {
+ Uplink::Send("ENDBURST");
+ }
+
+ void SendGlobops(const MessageSource &source, const Anope::string &buf) override
+ {
+ if (Servers::Capab.count("GLOBOPS"))
+ Uplink::Send(source, "SNONOTICE", 'g', buf);
+ else
+ Uplink::Send(source, "SNONOTICE", "A", buf);
+ }
+
+ void SendLogin(User *u, NickAlias *na) override
+ {
+ /* InspIRCd uses an account to bypass chmode +R, not umode +r, so we can't send this here */
+ if (!na->nc->HasExt("UNCONFIRMED"))
+ SendAccount(u->GetUID(), na);
+ }
+
+ void SendLogout(User *u) override
+ {
+ SendAccount(u->GetUID(), nullptr);
+ }
+
+ void SendChannel(Channel *c) override
+ {
+ Uplink::Send("FJOIN", c->name, c->creation_time, "+" + c->GetModes(true, true), "");
+ }
+
+ void SendSASLMessage(const SASL::Message &message) override
+ {
+ if (message.ext.empty())
+ Uplink::Send("ENCAP", message.target.substr(0, 3), "SASL", message.source, message.target, message.type, message.data);
+ else
+ Uplink::Send("ENCAP", message.target.substr(0, 3), "SASL", message.source, message.target, message.type, message.data, message.ext);
+ }
+
+ void SendSVSLogin(const Anope::string &uid, NickAlias *na) override
+ {
+ SendAccount(uid, na);
+
+ // Expire old pending sessions or other sessions for this user.
+ for (auto it = saslusers.begin(); it != saslusers.end(); )
+ {
+ const SASLUser &u = *it;
+ if (u.created + 30 < Anope::CurTime || u.uid == uid)
+ it = saslusers.erase(it);
+ else
+ ++it;
+ }
+
+ if (na)
+ {
+ if (!na->GetVhostIdent().empty())
+ Uplink::Send("ENCAP", uid.substr(0, 3), "CHGIDENT", uid, na->GetVhostIdent());
+
+ if (!na->GetVhostHost().empty())
+ Uplink::Send("ENCAP", uid.substr(0, 3), "CHGHOST", uid, na->GetVhostHost());
+
+ // Mark this SASL session as pending user introduction.
+ SASLUser su;
+ su.uid = uid;
+ su.acc = na->nc->display;
+ su.created = Anope::CurTime;
+ saslusers.push_back(su);
+ }
+ }
+
+ bool IsExtbanValid(const Anope::string &mask) override
+ {
+ return mask.length() >= 3 && mask[1] == ':';
+ }
+
+ bool IsIdentValid(const Anope::string &ident) override
+ {
+ if (ident.empty() || ident.length() > IRCD->GetMaxUser())
+ return false;
+
+ for (auto c : ident)
+ {
+ if (c >= 'A' && c <= '}')
+ continue;
+
+ if ((c >= '0' && c <= '9') || c == '-' || c == '.')
+ continue;
+
+ return false;
+ }
+
+ return true;
+ }
+
+ bool IsTagValid(const Anope::string &tname, const Anope::string &tvalue) override
+ {
+ // InspIRCd accepts arbitrary message tags.
+ return true;
+ }
+};
+
+class InspIRCdAutoOpMode final
+ : public ChannelModeList
+{
+public:
+ InspIRCdAutoOpMode(char mode) : ChannelModeList("AUTOOP", mode)
+ {
+ }
+
+ bool IsValid(Anope::string &mask) const override
+ {
+ // We can not validate this because we don't know about the
+ // privileges of the setter so just reject attempts to set it.
+ return false;
+ }
+};
+
+// NOTE: matchers for the following extbans have not been implemented:
+//
+// * class(n): data not available
+// * country(G): data not available
+// * gateway(w): data not available in v3
+// * realmask(a): todo
+class InspIRCdExtBan
+ : public ChannelModeVirtual<ChannelModeList>
+{
+ char ext;
+
+public:
+ InspIRCdExtBan(const Anope::string &mname, const Anope::string &basename, char extban) : ChannelModeVirtual<ChannelModeList>(mname, basename)
+ , ext(extban)
+ {
+ }
+
+ ChannelMode *Wrap(Anope::string &param) override
+ {
+ param = Anope::string(ext) + ":" + param;
+ return ChannelModeVirtual<ChannelModeList>::Wrap(param);
+ }
+
+ ChannelMode *Unwrap(ChannelMode *cm, Anope::string &param) override
+ {
+ if (cm->type != MODE_LIST || param.length() < 3 || param[0] != ext || param[1] != ':')
+ return cm;
+
+ param = param.substr(2);
+ return this;
+ }
+};
+
+namespace InspIRCdExtban
+{
+ class EntryMatcher final
+ : public InspIRCdExtBan
+ {
+ public:
+ EntryMatcher(const Anope::string &mname, const Anope::string &mbase, char c) : InspIRCdExtBan(mname, mbase, c)
+ {
+ }
+
+ bool Matches(User *u, const Entry *e) override
+ {
+ const Anope::string &mask = e->GetMask();
+ Anope::string real_mask = mask.substr(2);
+
+ return Entry(this->name, real_mask).Matches(u);
+ }
+ };
+
+ class ChannelMatcher final
+ : public InspIRCdExtBan
+ {
+ public:
+ ChannelMatcher(const Anope::string &mname, const Anope::string &mbase, char c) : InspIRCdExtBan(mname, mbase, c)
+ {
+ }
+
+ bool Matches(User *u, const Entry *e) override
+ {
+ const Anope::string &mask = e->GetMask();
+
+ Anope::string channel = mask.substr(2);
+
+ ChannelMode *cm = NULL;
+ if (channel[0] != '#')
+ {
+ char modeChar = ModeManager::GetStatusChar(channel[0]);
+ channel.erase(channel.begin());
+ cm = ModeManager::FindChannelModeByChar(modeChar);
+ if (cm != NULL && cm->type != MODE_STATUS)
+ cm = NULL;
+ }
+
+ Channel *c = Channel::Find(channel);
+ if (c != NULL)
+ {
+ ChanUserContainer *uc = c->FindUser(u);
+ if (uc != NULL)
+ if (cm == NULL || uc->status.HasMode(cm->mchar))
+ return true;
+ }
+
+ return false;
+ }
+ };
+
+ class AccountMatcher final
+ : public InspIRCdExtBan
+ {
+ public:
+ AccountMatcher(const Anope::string &mname, const Anope::string &mbase, char c) : InspIRCdExtBan(mname, mbase, c)
+ {
+ }
+
+ bool Matches(User *u, const Entry *e) override
+ {
+ const Anope::string &mask = e->GetMask();
+ Anope::string real_mask = mask.substr(2);
+
+ return u->IsIdentified() && real_mask.equals_ci(u->Account()->display);
+ }
+ };
+
+ class RealnameMatcher final
+ : public InspIRCdExtBan
+ {
+ public:
+ RealnameMatcher(const Anope::string &mname, const Anope::string &mbase, char c) : InspIRCdExtBan(mname, mbase, c)
+ {
+ }
+
+ bool Matches(User *u, const Entry *e) override
+ {
+ const Anope::string &mask = e->GetMask();
+ Anope::string real_mask = mask.substr(2);
+ return Anope::Match(u->realname, real_mask);
+ }
+ };
+
+ class ServerMatcher final
+ : public InspIRCdExtBan
+ {
+ public:
+ ServerMatcher(const Anope::string &mname, const Anope::string &mbase, char c) : InspIRCdExtBan(mname, mbase, c)
+ {
+ }
+
+ bool Matches(User *u, const Entry *e) override
+ {
+ const Anope::string &mask = e->GetMask();
+ Anope::string real_mask = mask.substr(2);
+ return Anope::Match(u->server->GetName(), real_mask);
+ }
+ };
+
+ class FingerprintMatcher final
+ : public InspIRCdExtBan
+ {
+ public:
+ FingerprintMatcher(const Anope::string &mname, const Anope::string &mbase, char c) : InspIRCdExtBan(mname, mbase, c)
+ {
+ }
+
+ bool Matches(User *u, const Entry *e) override
+ {
+ const Anope::string &mask = e->GetMask();
+ Anope::string real_mask = mask.substr(2);
+ return !u->fingerprint.empty() && Anope::Match(u->fingerprint, real_mask);
+ }
+ };
+
+ class UnidentifiedMatcher final
+ : public InspIRCdExtBan
+ {
+ public:
+ UnidentifiedMatcher(const Anope::string &mname, const Anope::string &mbase, char c) : InspIRCdExtBan(mname, mbase, c)
+ {
+ }
+
+ bool Matches(User *u, const Entry *e) override
+ {
+ const Anope::string &mask = e->GetMask();
+ Anope::string real_mask = mask.substr(2);
+ return !u->Account() && Entry("BAN", real_mask).Matches(u);
+ }
+ };
+
+ class OperTypeMatcher
+ : public InspIRCdExtBan
+ {
+ public:
+ OperTypeMatcher(const Anope::string &mname, const Anope::string &mbase, char c) : InspIRCdExtBan(mname, mbase, c)
+ {
+ }
+
+ bool Matches(User *u, const Entry *e) anope_override
+ {
+ Anope::string *opertype = u->GetExt<Anope::string>("opertype");
+ if (!opertype)
+ return false; // Not an operator.
+
+ const Anope::string &mask = e->GetMask();
+ Anope::string real_mask = mask.substr(2);
+ return Anope::Match(opertype->replace_all_cs(' ', '_'), real_mask);
+ }
+ };
+}
+
+class ColonDelimitedParamMode
+ : public ChannelModeParam
+{
+public:
+ ColonDelimitedParamMode(const Anope::string &modename, char modeChar) : ChannelModeParam(modename, modeChar, true) { }
+
+ bool IsValid(Anope::string &value) const override
+ {
+ return IsValid(value, false);
+ }
+
+ static bool IsValid(const Anope::string &value, bool historymode)
+ {
+ if (value.empty())
+ return false; // empty param is never valid
+
+ Anope::string::size_type pos = value.find(':');
+ if ((pos == Anope::string::npos) || (pos == 0))
+ return false; // no ':' or it's the first char, both are invalid
+
+ Anope::string rest;
+ try
+ {
+ if (convertTo<int>(value, rest, false) <= 0)
+ return false; // negative numbers and zero are invalid
+
+ rest = rest.substr(1);
+ int n;
+ if (historymode)
+ {
+ // For the history mode, the part after the ':' is a duration and it
+ // can be in the user friendly "1d3h20m" format, make sure we accept that
+ n = Anope::DoTime(rest);
+ }
+ else
+ n = convertTo<int>(rest);
+
+ if (n <= 0)
+ return false;
+ }
+ catch (const ConvertException &e)
+ {
+ // conversion error, invalid
+ return false;
+ }
+
+ return true;
+ }
+};
+
+class SimpleNumberParamMode final
+ : public ChannelModeParam
+{
+public:
+ SimpleNumberParamMode(const Anope::string &modename, char modeChar) : ChannelModeParam(modename, modeChar, true) { }
+
+ bool IsValid(Anope::string &value) const override
+ {
+ if (value.empty())
+ return false; // empty param is never valid
+
+ try
+ {
+ if (convertTo<int>(value) <= 0)
+ return false;
+ }
+ catch (const ConvertException &e)
+ {
+ // conversion error, invalid
+ return false;
+ }
+
+ return true;
+ }
+};
+
+class ChannelModeFlood final
+ : public ColonDelimitedParamMode
+{
+public:
+ ChannelModeFlood(char modeChar) : ColonDelimitedParamMode("FLOOD", modeChar) { }
+
+ bool IsValid(Anope::string &value) const override
+ {
+ // The parameter of this mode is a bit different, it may begin with a '*',
+ // ignore it if that's the case
+ Anope::string v = value[0] == '*' ? value.substr(1) : value;
+ return ((!value.empty()) && (ColonDelimitedParamMode::IsValid(v)));
+ }
+};
+
+class ChannelModeHistory final
+ : public ColonDelimitedParamMode
+{
+public:
+ ChannelModeHistory(char modeChar) : ColonDelimitedParamMode("HISTORY", modeChar) { }
+
+ bool IsValid(Anope::string &value) const override
+ {
+ return (ColonDelimitedParamMode::IsValid(value, true));
+ }
+};
+
+class ChannelModeRedirect final
+ : public ChannelModeParam
+{
+public:
+ ChannelModeRedirect(char modeChar) : ChannelModeParam("REDIRECT", modeChar, true) { }
+
+ bool IsValid(Anope::string &value) const override
+ {
+ // The parameter of this mode is a channel, and channel names start with '#'
+ return ((!value.empty()) && (value[0] == '#'));
+ }
+};
+
+struct IRCDMessageAway final
+ : Message::Away
+{
+ IRCDMessageAway(Module *creator) : Message::Away(creator, "AWAY") { SetFlag(FLAG_REQUIRE_USER); }
+
+ void Run(MessageSource &source, const std::vector<Anope::string> &params, const Anope::map<Anope::string> &tags) override
+ {
+ std::vector<Anope::string> newparams(params);
+ if (newparams.size() > 1)
+ newparams.erase(newparams.begin());
+
+ Message::Away::Run(source, newparams, tags);
+ }
+};
+
+struct IRCDMessageCapab final
+ : Message::Capab
+{
+ struct ModeInfo final
+ {
+ // The letter assigned to the mode (e.g. o).
+ char letter = 0;
+
+ // If a prefix mode then the rank of the prefix.
+ unsigned level = 0;
+
+ // The name of the mode.
+ Anope::string name;
+
+ // If a prefix mode then the symbol associated with the prefix.
+ char symbol = 0;
+
+ // The type of mode.
+ Anope::string type;
+ };
+
+ static std::pair<Anope::string, size_t> ParseCapability(const Anope::string &token)
+ {
+ auto sep = token.find(':');
+ if (sep == Anope::string::npos)
+ return { token, 0 };
+
+ auto value = token.substr(sep);
+ if (!value.is_pos_number_only())
+ return { token, 0 };
+
+ return { token.substr(0, sep), convertTo<size_t>(value) };
+ }
+
+ static bool ParseMode(const Anope::string &token, ModeInfo &mode)
+ {
+ // list:ban=b param-set:limit=l param:key=k prefix:30000:op=@o simple:noextmsg=n
+ // A C A C A C A B C A C
+ Anope::string::size_type a = token.find(':');
+ if (a == Anope::string::npos)
+ return false;
+
+ // If the mode is a prefix mode then it also has a rank.
+ mode.type = token.substr(0, a);
+ if (mode.type == "prefix")
+ {
+ Anope::string::size_type b = token.find(':', a + 1);
+ if (b == Anope::string::npos)
+ return false;
+
+ const Anope::string modelevel = token.substr(a + 1, b - a - 1);
+ mode.level = modelevel.is_pos_number_only() ? convertTo<unsigned>(modelevel) : 0;
+ a = b;
+ }
+
+ Anope::string::size_type c = token.find('=', a + 1);
+ if (c == Anope::string::npos)
+ return false;
+
+ mode.name = token.substr(a + 1, c - a - 1);
+ switch (token.length() - c)
+ {
+ case 2:
+ mode.letter = token[c + 1];
+ break;
+ case 3:
+ mode.symbol = token[c + 1];
+ mode.letter = token[c + 2];
+ break;
+ default:
+ return false;
+ }
+
+ Log(LOG_DEBUG) << "Parsed mode: " << "type=" << mode.type << " name=" << mode.name << " level="
+ << mode.level << " symbol=" << mode.symbol << " letter=" << mode.letter;
+ return true;
+ }
+
+ IRCDMessageCapab(Module *creator) : Message::Capab(creator, "CAPAB") { SetFlag(FLAG_SOFT_LIMIT); }
+
+ void Run(MessageSource &source, const std::vector<Anope::string> &params, const Anope::map<Anope::string> &tags) override
+ {
+ if (params[0].equals_cs("START"))
+ {
+ unsigned int spanningtree_proto_ver = 0;
+ if (params.size() >= 2)
+ spanningtree_proto_ver = params[1].is_pos_number_only() ? convertTo<unsigned>(params[1]) : 0;
+
+ if (spanningtree_proto_ver < 1205)
+ {
+ Uplink::Send("ERROR", "Protocol mismatch, no or invalid protocol version given in CAPAB START.");
+ Anope::QuitReason = "Protocol mismatch, no or invalid protocol version given in CAPAB START";
+ Anope::Quitting = true;
+ return;
+ }
+
+ /* reset CAPAB */
+ Servers::Capab.insert("SERVERS");
+ Servers::Capab.insert("TOPICLOCK");
+ IRCD->CanSQLineChannel = false;
+ IRCD->CanSVSHold = false;
+ IRCD->DefaultPseudoclientModes = "+oI";
+ }
+ else if (params[0].equals_cs("CHANMODES") && params.size() > 1)
+ {
+ spacesepstream ssep(params[1]);
+ Anope::string capab;
+
+ while (ssep.GetToken(capab))
+ {
+ ModeInfo mode;
+ if (!ParseMode(capab, mode))
+ continue;
+
+ ChannelMode *cm = NULL;
+ if (mode.name.equals_cs("admin"))
+ cm = new ChannelModeStatus("PROTECT", mode.letter, mode.symbol, mode.level);
+ else if (mode.name.equals_cs("allowinvite"))
+ {
+ cm = new ChannelMode("ALLINVITE", mode.letter);
+ ModeManager::AddChannelMode(new InspIRCdExtban::EntryMatcher("INVITEBAN", "BAN", 'A'));
+ }
+ else if (mode.name.equals_cs("auditorium"))
+ cm = new ChannelMode("AUDITORIUM", mode.letter);
+ else if (mode.name.equals_cs("autoop"))
+ cm = new InspIRCdAutoOpMode(mode.letter);
+ else if (mode.name.equals_cs("ban"))
+ cm = new ChannelModeList("BAN", mode.letter);
+ else if (mode.name.equals_cs("banexception"))
+ cm = new ChannelModeList("EXCEPT", mode.letter);
+ else if (mode.name.equals_cs("blockcaps"))
+ {
+ cm = new ChannelMode("BLOCKCAPS", mode.letter);
+ ModeManager::AddChannelMode(new InspIRCdExtban::EntryMatcher("BLOCKCAPSBAN", "BAN", 'B'));
+ }
+ else if (mode.name.equals_cs("blockcolor"))
+ {
+ cm = new ChannelMode("BLOCKCOLOR", mode.letter);
+ ModeManager::AddChannelMode(new InspIRCdExtban::EntryMatcher("BLOCKCOLORBAN", "BAN", 'c'));
+ }
+ else if (mode.name.equals_cs("c_registered"))
+ cm = new ChannelModeNoone("REGISTERED", mode.letter);
+ else if (mode.name.equals_cs("censor"))
+ cm = new ChannelMode("CENSOR", mode.letter);
+ else if (mode.name.equals_cs("delayjoin"))
+ cm = new ChannelMode("DELAYEDJOIN", mode.letter);
+ else if (mode.name.equals_cs("delaymsg"))
+ cm = new SimpleNumberParamMode("DELAYMSG", mode.letter);
+ else if (mode.name.equals_cs("filter"))
+ cm = new ChannelModeList("FILTER", mode.letter);
+ else if (mode.name.equals_cs("flood"))
+ cm = new ChannelModeFlood(mode.letter);
+ else if (mode.name.equals_cs("founder"))
+ cm = new ChannelModeStatus("OWNER", mode.letter, mode.symbol, mode.level);
+ else if (mode.name.equals_cs("halfop"))
+ cm = new ChannelModeStatus("HALFOP", mode.letter, mode.symbol, mode.level);
+ else if (mode.name.equals_cs("history"))
+ cm = new ChannelModeHistory(mode.letter);
+ else if (mode.name.equals_cs("invex"))
+ cm = new ChannelModeList("INVITEOVERRIDE", mode.letter);
+ else if (mode.name.equals_cs("inviteonly"))
+ cm = new ChannelMode("INVITE", mode.letter);
+ else if (mode.name.equals_cs("joinflood"))
+ cm = new ColonDelimitedParamMode("JOINFLOOD", mode.letter);
+ else if (mode.name.equals_cs("key"))
+ cm = new ChannelModeKey(mode.letter);
+ else if (mode.name.equals_cs("kicknorejoin"))
+ cm = new SimpleNumberParamMode("NOREJOIN", mode.letter);
+ else if (mode.name.equals_cs("limit"))
+ cm = new ChannelModeParam("LIMIT", mode.letter, true);
+ else if (mode.name.equals_cs("moderated"))
+ cm = new ChannelMode("MODERATED", mode.letter);
+ else if (mode.name.equals_cs("nickflood"))
+ cm = new ColonDelimitedParamMode("NICKFLOOD", mode.letter);
+ else if (mode.name.equals_cs("noctcp"))
+ {
+ cm = new ChannelMode("NOCTCP", mode.letter);
+ ModeManager::AddChannelMode(new InspIRCdExtban::EntryMatcher("NOCTCPBAN", "BAN", 'C'));
+ }
+ else if (mode.name.equals_cs("noextmsg"))
+ cm = new ChannelMode("NOEXTERNAL", mode.letter);
+ else if (mode.name.equals_cs("nokick"))
+ {