aboutsummaryrefslogtreecommitdiff
path: root/src/modules
diff options
context:
space:
mode:
authorGravatar Sadie Powell2026-02-13 16:32:24 +0000
committerGravatar Sadie Powell2026-02-16 13:31:15 +0000
commitc840c8b858f3a9d0c34a6130ee1fab3641b5dee7 (patch)
tree76035873c428cd67d87ddc29f58fd6956edf7fa3 /src/modules
parentFix checking whether the +z channel mode is already set. (diff)
Add support for filtering message tags to the known keys.
Diffstat (limited to 'src/modules')
-rw-r--r--src/modules/m_ircv3_ctctags.cpp103
1 files changed, 95 insertions, 8 deletions
diff --git a/src/modules/m_ircv3_ctctags.cpp b/src/modules/m_ircv3_ctctags.cpp
index 397185e57..7fcd3ffcd 100644
--- a/src/modules/m_ircv3_ctctags.cpp
+++ b/src/modules/m_ircv3_ctctags.cpp
@@ -24,6 +24,18 @@
#include "modules/isupport.h"
#include "numerichelper.h"
+enum class AllowTags
+{
+ // No client tags are allowed.
+ NONE,
+
+ // Only known client tags are allowed.
+ KNOWN,
+
+ // All client tags are allowed.
+ ALL,
+};
+
class CommandTagMsg final
: public Command
{
@@ -279,8 +291,47 @@ class C2CTags final
private:
Cap::Capability& cap;
+ static bool ValidateChannel(LocalUser* user, const std::string& tagvalue)
+ {
+ for (const auto* memb : user->chans)
+ {
+ if (irc::equals(memb->chan->name, tagvalue))
+ return true;
+ }
+ return false;
+ }
+
+ static bool ValidateMessageId(LocalUser* user, const std::string& tagvalue)
+ {
+ // Message IDs MUST NOT start with a colon (:) and MUST NOT contain any
+ // of SPACE, CR, LF.
+ return !tagvalue.empty()
+ && tagvalue[0] != ':'
+ && tagvalue.find_first_of(" \n\r") != std::string::npos;
+ }
+
+ static bool ValidateTyping(LocalUser* user, const std::string& tagvalue)
+ {
+ // A typing notification is represented by a TAGMSG command sent with a
+ // typing tag using the client-only prefix + and possible values of
+ // active, paused, and done.
+ return irc::equals(tagvalue, "active")
+ || irc::equals(tagvalue, "paused")
+ || irc::equals(tagvalue, "done");
+ }
+
public:
- bool allowclientonlytags;
+ AllowTags allowclientonlytags;
+ insp::flat_map<std::string, std::function<bool(LocalUser*, const std::string&)>, irc::insensitive_swo> knowntags = {
+ { "+draft/channel-context", ValidateChannel }, // https://ircv3.net/specs/client-tags/channel-context
+ { "+draft/react", ValidateMessageId }, // https://ircv3.net/specs/client-tags/react
+ { "+draft/reply", ValidateMessageId }, // https://ircv3.net/specs/client-tags/reply
+ { "+draft/unreact", ValidateMessageId }, // https://ircv3.net/specs/client-tags/react
+
+ { "+reply", ValidateMessageId }, // https://ircv3.net/specs/client-tags/reply
+ { "+typing", ValidateTyping }, // https://ircv3.net/specs/client-tags/typing
+ };
+
C2CTags(Module* Creator, Cap::Capability& Cap)
: ClientProtocol::MessageTagProvider(Creator)
, cap(Cap)
@@ -291,14 +342,24 @@ public:
{
// A client-only tag is prefixed with a plus sign (+) and otherwise conforms
// to the format specified in IRCv3.2 tags.
- if (tagname[0] != '+' || tagname.length() < 2 || !allowclientonlytags)
+ if (tagname[0] != '+' || tagname.length() < 2 || allowclientonlytags == AllowTags::NONE)
return MOD_RES_PASSTHRU;
// If the user is local then we check whether they have the message-tags cap
// enabled. If not then we reject all client-only tags originating from them.
- LocalUser* lu = IS_LOCAL(user);
- if (lu && !cap.IsEnabled(lu))
- return MOD_RES_DENY;
+ auto* lu = IS_LOCAL(user);
+ if (lu)
+ {
+ if (!cap.IsEnabled(lu))
+ return MOD_RES_DENY; // Cap not enabled.
+
+ if (allowclientonlytags == AllowTags::KNOWN)
+ {
+ auto it = knowntags.find(tagname);
+ if (it == knowntags.end() || !it->second(lu, tagvalue))
+ return MOD_RES_DENY; // Tag not whitelisted.
+ }
+ }
// Remote users have their client-only tags checked by their local server.
return MOD_RES_ALLOW;
@@ -348,13 +409,39 @@ public:
void ReadConfig(ConfigStatus& status) override
{
- c2ctags.allowclientonlytags = ServerInstance->Config->ConfValue("ctctags")->getBool("allowclientonlytags", true);
+ const auto& tag = ServerInstance->Config->ConfValue("ctctags");
+ c2ctags.allowclientonlytags = tag->getEnum("allowclientonlytags", AllowTags::ALL, {
+ {"all", AllowTags::ALL },
+ {"known", AllowTags::KNOWN },
+ {"none", AllowTags::NONE },
+
+ // Compatibility with v4.9 and older.
+ {"false", AllowTags::NONE },
+ {"no", AllowTags::NONE },
+ {"off", AllowTags::NONE },
+ {"on", AllowTags::ALL },
+ {"true", AllowTags::ALL },
+ {"yes", AllowTags::ALL },
+ });
}
void OnBuildISupport(ISupport::TokenMap& tokens) override
{
- if (!c2ctags.allowclientonlytags)
- tokens["CLIENTTAGDENY"] = "*";
+ switch (c2ctags.allowclientonlytags)
+ {
+ case AllowTags::NONE:
+ tokens["CLIENTTAGDENY"] = "*";
+ break;
+ case AllowTags::KNOWN:
+ {
+ auto& token = tokens["CLIENTTAGDENY"] = "*";
+ for (const auto& [tag, _] : c2ctags.knowntags)
+ token.append(",-").append(tag, 1, std::string::npos);
+ break;
+ }
+ case AllowTags::ALL:
+ break;
+ }
}
ModResult OnUserPreMessage(User* user, MessageTarget& target, MessageDetails& details) override