/*
* InspIRCd -- Internet Relay Chat Daemon
*
* Copyright (C) 2019-2023, 2026 Sadie Powell <sadie@witchery.services>
* Copyright (C) 2017 Dylan Frank <b00mx0r@aureus.pw>
* Copyright (C) 2012-2014 Attila Molnar <attilamolnar@hush.com>
* Copyright (C) 2012, 2014 Shawn Smith <ShawnSmith0828@gmail.com>
* Copyright (C) 2012 Robby <robby@chatbelgie.be>
* Copyright (C) 2009 Daniel De Graaf <danieldg@inspircd.org>
* Copyright (C) 2008 Robin Burchell <robin+git@viroteck.net>
* Copyright (C) 2006 Craig Edwards <brain@inspircd.org>
*
* This file is part of InspIRCd. InspIRCd is free software: you can
* redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, version 2.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "inspircd.h"
#include "extension.h"
#include "modules/extban.h"
#include "numerichelper.h"
enum
{
// From UnrealIRCd?
ERR_LINKCHANNEL = 470,
// InspIRCd-specific.
ERR_REDIRECT = 690,
};
namespace
{
Channel* CheckRedirect(User* source, Channel* channel, const std::string& parameter)
{
if (!ServerInstance->Channels.IsChannel(parameter))
{
source->WriteNumeric(Numerics::NoSuchChannel(parameter));
return nullptr;
}
auto* c = ServerInstance->Channels.Find(parameter);
if (!source->IsOper())
{
if (!c)
{
source->WriteNumeric(ERR_REDIRECT, parameter, FMT::format("Target channel {} must exist to be set as a redirect.", parameter));
return nullptr;
}
else if (c->GetPrefixValue(source) < OP_VALUE)
{
source->WriteNumeric(ERR_REDIRECT, c->name, FMT::format("You must be opped on {} to set it as a redirect.", c->name));
return nullptr;
}
}
return c;
}
}
class RedirectExtBan final
: public ExtBan::Acting
{
private:
bool SplitBan(const std::string& text, std::string& chan, std::string& mask)
{
auto sep = text.find(':');
if (sep == std::string::npos || sep == 0 || sep+1 >= text.length())
return false; // Malformed.
chan.assign(text, 0, sep);
mask.assign(text, sep + 1);
return true;
}
public:
std::string matchchan;
RedirectExtBan(const WeakModulePtr& mod)
: ExtBan::Acting(mod, "redirect", 'd')
{
}
bool IsMatch(ListModeBase* lm, User* user, Channel* channel, const std::string& text, const ExtBan::MatchConfig& config) override
{
std::string target, mask;
if (!SplitBan(text, target, mask))
return false; // Malformed.
matchchan = target;
return ExtBan::Acting::IsMatch(lm, user, channel, mask, config);
}
bool Validate(ListModeBase* lm, LocalUser* user, Channel* channel, std::string& text) override
{
std::string target, mask;
if (!SplitBan(text, target, mask))
{
user->WriteNumeric(ERR_REDIRECT, text, "Redirect extban must be in the format <chan>:<mask>.");
return false; // Malformed.
}
auto* targetchan = CheckRedirect(user, channel, target);
if (!targetchan)
return false; // Bad target.
Canonicalize(mask);
text = FMT::format("{}:{}", targetchan->name, mask);
return true;
}
};
class RedirectMode final
: public ParamMode<RedirectMode, StringExtItem>
{
public:
RedirectMode(const WeakModulePtr& mod)
: ParamMode<RedirectMode, StringExtItem>(mod, "redirect", 'L')
{
syntax = "<target>";
}
bool OnSet(User* source, Channel* channel, std::string& parameter) override
{
if (source->IsLocal())
{
auto* targetchan = CheckRedirect(source, channel, parameter);
if (!targetchan)
return false;
parameter = targetchan->name;
}
ext.Set(channel, parameter);
return true;
}
void SerializeParam(Channel* chan, const std::string* str, std::string& out)
{
out += *str;
}
};
class BanRedirect final
: public ModeWatcher
{
private:
RedirectExtBan& redirectextban;
public:
BanRedirect(const WeakModulePtr& mod, RedirectExtBan& extban)
: ModeWatcher(mod, "ban", MODETYPE_CHANNEL)
, redirectextban(extban)
{
}
bool BeforeMode(User* source, User* dest, Channel* channel, Modes::Change& change) override
{
if (!change.adding || change.param.empty() || !source->IsLocal())
return true; // Nothing to do.
bool xbinverted; // Unused
std::string xbname, xbvalue; // Unused
if (ExtBan::Parse(change.param, xbname, xbvalue, xbinverted))
return true; // Don't touch extbans
const auto pos_of_pling = change.param.find_first_of('!');
const auto pos_of_at = change.param.find_first_of('@', pos_of_pling == std::string::npos ? 0 : pos_of_pling);
const auto pos_of_hash = change.param.find_first_of('#', pos_of_at == std::string::npos ? 0 : pos_of_at);
if (pos_of_hash != std::string::npos)
{
// Rewrite old banredirect entries from foo!bar@baz#chan to redirect:#chan:foo!bar@baz
change.param = FMT::format("{}:{}:{}", redirectextban.service_name,
change.param.substr(pos_of_hash), change.param.substr(0, pos_of_hash));
}
return true;
}
};
class ModuleRedirect final
: public Module
{
private:
bool action_ban;
bool action_inviteonly;
bool action_key;
bool action_limit;
std::string activechan;
SimpleUserMode antiredirectmode;
ChanModeReference banmode;
ChanModeReference inviteonlymode;
ChanModeReference keymode;
ChanModeReference limitmode;
RedirectExtBan redirectextban;
RedirectMode redirectmode;
BanRedirect banredirect;
ModResult HandleRedirect(LocalUser* user, Channel* chan, const std::string& reason, bool param = true)
{
const auto* channel = param ? redirectmode.ext.Get(chan) : &redirectextban.matchchan;
if (!channel)
return MOD_RES_PASSTHRU; // Should never happen.
// Sometimes broken services can make circular or chained +L, avoid this.
if (!activechan.empty())
{
user->WriteNumeric(ERR_LINKCHANNEL, activechan, channel, "You may not join this channel. A redirect is set, but you cannot be redirected as it is a circular loop.");
return MOD_RES_PASSTHRU;
}
if (user->IsModeSet(antiredirectmode))
{
user->WriteNumeric(ERR_LINKCHANNEL, chan->name, *channel,
FMT::format("You cannot join {} ({}) and you cannot be redirected to {} as you have user mode +{} ({}) set.",
chan->name, reason, *channel, antiredirectmode.GetModeChar(), antiredirectmode.service_name));
return MOD_RES_DENY;
}
user->WriteNumeric(ERR_LINKCHANNEL, chan->name, *channel,
FMT::format("You cannot join {} ({}) so you are being automatically transferred to {}.",
chan->name, reason, *channel));
activechan = chan->name;
Channel::JoinUser(user, *channel);
activechan.clear();
return MOD_RES_DENY;
}
public:
ModuleRedirect()
: Module(VF_VENDOR, "Allows users to be redirected to another channel.")
, antiredirectmode(weak_from_this(), "antiredirect", 'L')
, banmode(weak_from_this(), "ban")
, inviteonlymode(weak_from_this(), "inviteonly")
, keymode(weak_from_this(), "key")
, limitmode(weak_from_this(), "limit")
, redirectextban(weak_from_this())
, redirectmode(weak_from_this())
, banredirect(weak_from_this(), redirectextban)
{
}
void ReadConfig(ConfigStatus&) override
{
const auto& tag = ServerInstance->Config->ConfValue("redirect");
action_ban = tag->getBool("ban", false);
action_inviteonly = tag->getBool("inviteonly", true);
action_key = tag->getBool("key", true);
action_limit = tag->getBool("limit", true);
}
ModResult OnUserPreJoin(LocalUser* user, Channel* chan, const std::string& cname, PrefixMode::Set& privs, const std::string& keygiven, bool override) override
{
if (override || !chan)
return MOD_RES_PASSTHRU; // No redirect possible.
auto modres = redirectextban.GetStatus(user, chan);
if (modres == MOD_RES_DENY)
return HandleRedirect(user, chan, "you're extbanned", false);
if (!chan->IsModeSet(redirectmode))
return MOD_RES_PASSTHRU; // All others require the mode.
if (action_ban && chan->CheckList(*banmode, user))
return HandleRedirect(user, chan, "you're banned");
if (action_inviteonly && chan->IsModeSet(inviteonlymode))
{
FIRST_MOD_RESULT(OnCheckInvite, modres, (user, chan));
if (modres != MOD_RES_ALLOW)
return HandleRedirect(user, chan, "invite only");
}
if (action_key)
{
const auto key = chan->GetModeParameter(keymode);
if (!key.empty())
{
FIRST_MOD_RESULT(OnCheckKey, modres, (user, chan, keygiven));
if (!modres.check(InspIRCd::TimingSafeCompare(key, keygiven)))
return HandleRedirect(user, chan, "incorrect channel key");
}
}
if (action_limit)
{
const auto limit = chan->GetModeParameter(limitmode);
if (!limit.empty())
{
FIRST_MOD_RESULT(OnCheckLimit, modres, (user, chan));
if (!modres.check(chan->GetUsers().size() < ConvToNum<size_t>(limit)))
return HandleRedirect(user, chan, "limit reached");
}
}
return MOD_RES_PASSTHRU;
}
};
MODULE_INIT(ModuleRedirect)