Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add m_relaymsg: stateless bridging using RELAYMSG #248

Merged
merged 7 commits into from
Dec 21, 2020
212 changes: 212 additions & 0 deletions 3.0/m_relaymsg.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* InspIRCd -- Internet Relay Chat Daemon
*
* Copyright (C) 2020 James Lu <james@overdrivenetworks.com>
*
* This file is a module for 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/>.
*/

/// $ModAuthor: jlu5
/// $ModAuthorMail: james@overdrivenetworks.com
/// $ModDepends: core 3
/// $ModDesc: Provides the RELAYMSG command & draft/relaymsg capability for stateless bridging
/// $ModConfig: <relaymsg separators="/" ident="relay" host="relay.example.com">
// The "host" option defaults to the local server hostname if not set.

#include "inspircd.h"
#include "modules/cap.h"
#include "modules/ircv3.h"

enum
{
ERR_BADRELAYNICK = 573 // from ERR_CANNOTSENDRP in Oragono
};

// Registers the draft/relaymsg
class RelayMsgCap : public Cap::Capability
{
public:
std::string nick_separators;

const std::string* GetValue(LocalUser* user) const CXX11_OVERRIDE
{
return &nick_separators;
}

RelayMsgCap(Module* mod)
: Cap::Capability(mod, "draft/relaymsg")
{
}
};

// Handler for the @relaymsg message tag sent with forwarded PRIVMSGs
class RelayMsgCapTag : public ClientProtocol::MessageTagProvider {
private:
RelayMsgCap& cap;

public:
RelayMsgCapTag(Module* mod, RelayMsgCap& Cap)
: ClientProtocol::MessageTagProvider(mod)
, cap(Cap)
{
}

bool ShouldSendTag(LocalUser* user, const ClientProtocol::MessageTagData& tagdata) CXX11_OVERRIDE
{
return cap.get(user);
}
};

// Handler for the RELAYMSG command (users and servers)
class CommandRelayMsg : public Command
{
private:
RelayMsgCap& cap;
RelayMsgCapTag& captag;

public:
std::string fake_host;
std::string fake_ident;

CommandRelayMsg(Module* parent, RelayMsgCap& Cap, RelayMsgCapTag& Captag)
: Command(parent, "RELAYMSG", 3, 3)
, cap(Cap)
, captag(Captag)
{
flags_needed = 'o';
syntax = "<channel> <nick> <text>";
allow_empty_last_param = false;
}

std::string GetFakeHostmask(const std::string& nick) {
return InspIRCd::Format("%s!%s@%s", nick.c_str(), fake_ident.c_str(), fake_host.c_str());
}

CmdResult Handle(User* user, const CommandBase::Params& parameters)
{
const std::string& channame = parameters[0];
const std::string& nick = parameters[1];
const std::string& text = parameters[2];

// Check that the source has the relaymsg capability.
if (IS_LOCAL(user) && !cap.get(user)) {
user->WriteNumeric(ERR_NOPRIVILEGES, "You must support the draft/relaymsg capability to use this command.");
return CMD_FAILURE;
}

Channel* channel = ServerInstance->FindChan(channame);
// Make sure the channel exists and the sender is in the channel
if (!channel)
{
user->WriteNumeric(Numerics::NoSuchChannel(channame));
return CMD_FAILURE;
}
if (!channel->HasUser(user))
{
user->WriteNumeric(Numerics::CannotSendTo(channel, "You must be in the channel to use this command."));
return CMD_FAILURE;
}

// Check that target nick is not already in use
if (ServerInstance->FindNick(nick))
{
user->WriteNumeric(ERR_BADRELAYNICK, nick, "RELAYMSG spoofed nick is already in use");
return CMD_FAILURE;
}

// Make sure the nick doesn't include any core IRC characters (e.g. *, !)
// This should still be more flexible than regular nick checking - in particular
// we want to allow "/" and "~" for relayers
if (nick.find_first_of("!+%@&#$:'\"?*,.") != std::string::npos)
{
user->WriteNumeric(ERR_BADRELAYNICK, nick, "Invalid characters in spoofed nick");
return CMD_FAILURE;
}

// If the sender was a lcoal user, check that the target includes a nick separator
if (IS_LOCAL(user))
{
if (nick.find_first_of(cap.nick_separators) == std::string::npos)
{
user->WriteNumeric(ERR_BADRELAYNICK, nick, InspIRCd::Format("Spoofed nickname must include one of the following separators: %s", cap.nick_separators.c_str()));
return CMD_FAILURE;
}
}

// Send the message to everyone in the channel
std::string fakeSource = GetFakeHostmask(nick);
ClientProtocol::Messages::Privmsg privmsg(fakeSource, channel, text);
// Tag the message as @draft/relaymsg=<nick> so the sender can recognize it
// Also copy over tags set on the original /relaymsg command
privmsg.AddTags(parameters.GetTags());
privmsg.AddTag("draft/relaymsg", &captag, user->nick);
channel->Write(ServerInstance->GetRFCEvents().privmsg, privmsg);

if (IS_LOCAL(user))
{
// Pass the message on to other servers
CommandBase::Params params;
jlu5 marked this conversation as resolved.
Show resolved Hide resolved
// Preserve other message tags sent with the /relaymsg command
params.GetTags().insert(parameters.GetTags().begin(), parameters.GetTags().end());
params.push_back(channame);
params.push_back(nick);
params.push_back(":" + text);

ServerInstance->PI->BroadcastEncap("RELAYMSG", params, user);
}

return CMD_SUCCESS;
};
};

class ModuleRelayMsg : public Module
{
RelayMsgCap cap;
RelayMsgCapTag captag;
CommandRelayMsg cmd;

public:
ModuleRelayMsg() :
cap(this),
captag(this, cap),
cmd(this, cap, captag)
{
}

void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE
{
ConfigTag* tag = ServerInstance->Config->ConfValue("relaymsg");
std::string fake_ident = tag->getString("ident", "relay");
std::string fake_host = tag->getString("host", ServerInstance->Config->ServerName);

// Check these before replacement so that invalid values loaded during /rehash don't get saved
if (!ServerInstance->IsIdent(fake_ident))
{
throw ModuleException("Invalid ident value for <relaymsg>");
}
if (!ServerInstance->IsHost(fake_host))
{
throw ModuleException("Invalid host value for <relaymsg>");
}
cmd.fake_host = fake_host;
cmd.fake_ident = fake_ident;
cap.nick_separators = tag->getString("separators", "/", 1);
}

Version GetVersion() CXX11_OVERRIDE
{
return Version("Provides the RELAYMSG command for stateless bridging");
}
};

MODULE_INIT(ModuleRelayMsg)