diff options
Diffstat (limited to 'modules/protocol/inspircd.cpp')
| -rw-r--r-- | modules/protocol/inspircd.cpp | 2168 |
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> ¶ms) 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 ¶m) 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 ¶m) override + { + param = Anope::string(ext) + ":" + param; + return ChannelModeVirtual<ChannelModeList>::Wrap(param); + } + + ChannelMode *Unwrap(ChannelMode *cm, Anope::string ¶m) 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> ¶ms, 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> ¶ms, 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")) + { |
