diff options
author | Peter Powell <petpow@saberuk.com> | 2018-08-13 20:17:46 +0100 |
---|---|---|
committer | Peter Powell <petpow@saberuk.com> | 2018-08-13 21:51:11 +0100 |
commit | 58a0a7e01422e62de1565a8eb0a1febdc463d04d (patch) | |
tree | 8861789deefe9df3524690de8ccd11e5366f1f2e /include/clientprotocol.h | |
parent | e2a820cce21342478653a34cf8ce2b593128d035 (diff) |
Implement IRCv3 message tag support.
Co-authored-by: Attila Molnar <attilamolnar@hush.com>
Diffstat (limited to 'include/clientprotocol.h')
-rw-r--r-- | include/clientprotocol.h | 726 |
1 files changed, 726 insertions, 0 deletions
diff --git a/include/clientprotocol.h b/include/clientprotocol.h new file mode 100644 index 000000000..a3efcf984 --- /dev/null +++ b/include/clientprotocol.h @@ -0,0 +1,726 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2016 Attila Molnar <attilamolnar@hush.com> + * + * 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/>. + */ + + +#pragma once + +#include "event.h" + +namespace ClientProtocol +{ + class EventHook; + class MessageSource; + struct RFCEvents; + struct ParseOutput; + class TagSelection; +} + +/** Contains a message parsed from wire format. + * Used by Serializer::Parse(). + */ +struct ClientProtocol::ParseOutput +{ + /** Command name, must not be empty. + */ + std::string cmd; + + /** Parameter list, may be empty. + */ + ClientProtocol::ParamList params; + + /** Message tags, may be empty. + */ + ClientProtocol::TagMap tags; +}; + +/** A selection of zero or more tags in a TagMap. + */ +class ClientProtocol::TagSelection +{ + std::bitset<64> selection; + + public: + /** Check if a tag is selected. + * @param tags TagMap the tag is in. The TagMap must contain the same tags as it had when the tag + * was selected with Select(), otherwise the result is not meaningful. + * @param it Iterator to the tag to check. + * @return True if the tag is selected, false otherwise. + */ + bool IsSelected(const TagMap& tags, TagMap::const_iterator it) const + { + const size_t index = std::distance(tags.begin(), it); + return ((index < selection.size()) && (selection[index])); + } + + /** Select a tag. + * @param tags TagMap the tag is in. This parameter must be the same every time the method is called. + * The TagMap must not be altered otherwise the results of IsSelected() is not meaningful. + * @param it Iterator to the tag to mark as selected. + */ + void Select(const TagMap& tags, TagMap::const_iterator it) + { + const size_t index = std::distance(tags.begin(), it); + if (index < selection.size()) + selection[index] = true; + } + + /** Check if a TagSelection is equivalent to this object. + * @param other Other TagSelection object to compare this with. + * @return True if the objects are equivalent, false if they aren't. + */ + bool operator==(const TagSelection& other) const + { + return (this->selection == other.selection); + } +}; + +class ClientProtocol::MessageSource +{ + User* sourceuser; + const std::string* sourcestr; + + public: + /** Constructor, sets the source to be the full host of a user or sets it to be nothing. + * The actual source string when serializing will be obtained from User::GetFullHost() if the user is non-NULL. + * @param Sourceuser User to set as source of the message. If NULL, the message won't have a source when serialized. + * Optional, defaults to NULL. + */ + MessageSource(User* Sourceuser = NULL) + { + SetSourceUser(Sourceuser); + } + + /** Constructor, sets the source to the supplied string and optionally sets the source user. + * @param Sourcestr String to use as message source for serialization purposes. Must remain valid + * as long as this object is alive. + * @param Sourceuser User to set as source. Optional, defaults to NULL. It will not be used for serialization but + * if provided it may be used internally, for example to create message tags. + * Useful when the source string is synthesized but it is still related to a User. + */ + MessageSource(const std::string& Sourcestr, User* Sourceuser = NULL) + { + SetSource(Sourcestr, Sourceuser); + } + + /** Get the source of this message as a string. + * @return Pointer to the message source string or NULL if there is no source. + */ + const std::string* GetSource() const + { + // Return string if there's one explicitly set + if (sourcestr) + return sourcestr; + if (sourceuser) + return &sourceuser->GetFullHost(); + return NULL; + } + + /** Get the source User. + * This shouldn't be used for serialization, use GetSource() for that. + * @return User pointer if the message has a source user, NULL otherwise. + */ + User* GetSourceUser() const { return sourceuser; } + + /** Set the source of this message to a User. + * See the one parameter constructor for a more detailed description. + * @param Sourceuser User to set as source. + */ + void SetSourceUser(User* Sourceuser) + { + sourceuser = Sourceuser; + sourcestr = NULL; + } + + /** Set the source string and optionally source user. + * See the two parameter constructor for a more detailed description. + * @param Sourcestr String source, to be used for serialization purposes. Must remain valid as long + * as this object is alive. + * @param Sourceuser Source user to set, optional. + */ + void SetSource(const std::string& Sourcestr, User* Sourceuser = NULL) + { + sourcestr = &Sourcestr; + sourceuser = Sourceuser; + } + + /** Copy the source from a MessageSource object. + * @param other MessageSource object to copy from. + */ + void SetSource(const MessageSource& other) + { + sourcestr = other.sourcestr; + sourceuser = other.sourceuser; + } +}; + +/** Outgoing client protocol message. + * Represents a high level client protocol message which is turned into raw or wire format + * by a Serializer. Messages can be serialized into different format by different serializers. + * + * Messages are created on demand and are disposed of after they have been sent. + * + * All messages have a command name, a list of parameters and a map of tags, the last two can be empty. + * They also always have a source, see class MessageSource. + */ +class ClientProtocol::Message : public ClientProtocol::MessageSource +{ + public: + /** Contains information required to identify a specific version of a serialized message. + */ + struct SerializedInfo + { + const Serializer* serializer; + TagSelection tagwl; + + /** Constructor. + * @param Ser Serializer used to serialize the message. + * @param Tagwl Tag whitelist used to serialize the message. + */ + SerializedInfo(const Serializer* Ser, const TagSelection& Tagwl) + : serializer(Ser) + , tagwl(Tagwl) + { + } + + /** Check if a SerializedInfo object is equivalent to this object. + * @param other Other SerializedInfo object. + * @return True if other is equivalent to this object, false otherwise. + */ + bool operator==(const SerializedInfo& other) const + { + return ((serializer == other.serializer) && (tagwl == other.tagwl)); + } + }; + + class Param + { + const std::string* ptr; + insp::aligned_storage<std::string> str; + bool owned; + + void InitFrom(const Param& other) + { + owned = other.owned; + if (owned) + new(str) std::string(*other.str); + else + ptr = other.ptr; + } + + public: + operator const std::string&() const { return (owned ? *str : *ptr); } + + Param() + : ptr(NULL) + , owned(false) + { + } + + Param(const std::string& s) + : ptr(&s) + , owned(false) + { + } + + Param(int, const char* s) + : owned(true) + { + new(str) std::string(s); + } + + Param(int, const std::string& s) + : owned(true) + { + new(str) std::string(s); + } + + Param(const Param& other) + { + InitFrom(other); + } + + ~Param() + { + using std::string; + if (owned) + str->~string(); + } + + Param& operator=(const Param& other) + { + if (&other == this) + return *this; + + using std::string; + if (owned) + str->~string(); + InitFrom(other); + return *this; + } + + bool IsOwned() const { return owned; } + }; + + typedef std::vector<Param> ParamList; + + private: + typedef std::vector<std::pair<SerializedInfo, SerializedMessage> > SerializedList; + + ParamList params; + TagMap tags; + std::string command; + bool msginit_done; + mutable SerializedList serlist; + bool sideeffect; + + protected: + /** Set command string. + * @param cmd Command string to set. + */ + void SetCommand(const char* cmd) + { + command.clear(); + if (cmd) + command = cmd; + } + + public: + /** Constructor. + * @param cmd Command name, e.g. "JOIN", "NICK". May be NULL. If NULL, the command must be set + * with SetCommand() before the message is serialized. + * @param Sourceuser See the one parameter constructor of MessageSource for description. + */ + Message(const char* cmd, User* Sourceuser = NULL) + : ClientProtocol::MessageSource(Sourceuser) + , command(cmd ? cmd : std::string()) + , msginit_done(false) + , sideeffect(false) + { + params.reserve(8); + serlist.reserve(8); + } + + /** Constructor. + * @param cmd Command name, e.g. "JOIN", "NICK". May be NULL. If NULL, the command must be set + * with SetCommand() before the message is serialized. + * @param Sourcestr See the two parameter constructor of MessageSource for description. + * Must remain valid as long as this object is alive. + * @param Sourceuser See the two parameter constructor of MessageSource for description. + */ + Message(const char* cmd, const std::string& Sourcestr, User* Sourceuser = NULL) + : ClientProtocol::MessageSource(Sourcestr, Sourceuser) + , command(cmd ? cmd : std::string()) + , msginit_done(false) + , sideeffect(false) + { + params.reserve(8); + serlist.reserve(8); + } + + /** Get the parameters of this message. + * @return List of parameters. + */ + const ParamList& GetParams() const { return params; } + + /** Get a map of tags attached to this message. + * The map contains the tag providers that attached the tag to the message. + * @return Map of tags. + */ + const TagMap& GetTags() const { return tags; } + + /** Get the command string. + * @return Command string, e.g. "NICK", "001". + */ + const char* GetCommand() const { return command.c_str(); } + + /** Add a parameter to the parameter list. + * @param str String to add, will be copied. + */ + void PushParam(const char* str) { params.push_back(Param(0, str)); } + + /** Add a parameter to the parameter list. + * @param str String to add, will be copied. + */ + void PushParam(const std::string& str) { params.push_back(Param(0, str)); } + + /** Add a parameter to the parameter list. + * @param str String to add. + * The string will NOT be copied, it must remain alive until ClearParams() is called or until the object is destroyed. + */ + void PushParamRef(const std::string& str) { params.push_back(str); } + + /** Add a placeholder parameter to the parameter list. + * Placeholder parameters must be filled in later with actual parameters using ReplaceParam() or ReplaceParamRef(). + */ + void PushParamPlaceholder() { params.push_back(Param()); } + + /** Replace a parameter or a placeholder that is already in the parameter list. + * @param index Index of the parameter to replace. Must be less than GetParams().size(). + * @param str String to replace the parameter or placeholder with, will be copied. + */ + void ReplaceParam(unsigned int index, const char* str) { params[index] = Param(0, str); } + + /** Replace a parameter or a placeholder that is already in the parameter list. + * @param index Index of the parameter to replace. Must be less than GetParams().size(). + * @param str String to replace the parameter or placeholder with, will be copied. + */ + void ReplaceParam(unsigned int index, const std::string& str) { params[index] = Param(0, str); } + + /** Replace a parameter or a placeholder that is already in the parameter list. + * @param index Index of the parameter to replace. Must be less than GetParams().size(). + * @param str String to replace the parameter or placeholder with. + * The string will NOT be copied, it must remain alive until ClearParams() is called or until the object is destroyed. + */ + void ReplaceParamRef(unsigned int index, const std::string& str) { params[index] = Param(str); } + + /** Add a tag. + * @param tagname Raw name of the tag to use in the protocol. + * @param tagprov Provider of the tag. + * @param val Tag value. If empty no value will be sent with the tag. + * @param tagdata Tag provider specific data, will be passed to MessageTagProvider::ShouldSendTag(). Optional, defaults to NULL. + */ + void AddTag(const std::string& tagname, MessageTagProvider* tagprov, const std::string& val, void* tagdata = NULL) + { + tags.insert(std::make_pair(tagname, MessageTagData(tagprov, val, tagdata))); + } + + /** Add all tags in a TagMap to the tags in this message. Existing tags will not be overwritten. + * @param newtags New tags to add. + */ + void AddTags(const ClientProtocol::TagMap& newtags) + { + tags.insert(newtags.begin(), newtags.end()); + } + + /** Get the message in a serialized form. + * @param serializeinfo Information about which exact serialized form of the message is the caller asking for + * (which serializer to use and which tags to include). + * @return Serialized message according to serializeinfo. The returned reference remains valid until the + * next call to this method. + */ + const SerializedMessage& GetSerialized(const SerializedInfo& serializeinfo) const; + + /** Clear the parameter list and tags. + */ + void ClearParams() + { + msginit_done = false; + params.clear(); + tags.clear(); + InvalidateCache(); + } + + /** Remove all serialized messages. + * If a parameter is changed after the message has been sent at least once, this method must be called before + * serializing the message again to ensure the cache won't contain stale data. + */ + void InvalidateCache() + { + serlist.clear(); + } + + void CopyAll() + { + size_t j = 0; + for (ParamList::iterator i = params.begin(); i != params.end(); ++i, j++) + { + Param& curr = *i; + if (!curr.IsOwned()) + ReplaceParam(j, curr); + } + } + + void SetSideEffect(bool Sideeffect) { sideeffect = Sideeffect; } + bool IsSideEffect() const { return sideeffect; } + + friend class Serializer; +}; + +/** Client protocol event class. + * All messages sent to a user must be part of an event. A single event may result in more than one protocol message + * being sent, for example a join event may result in a JOIN and a MODE protocol message sent to members of the channel + * if the joining user has some prefix modes set. + * + * Event hooks attached to a specific event can alter the messages sent for that event. + */ +class ClientProtocol::Event +{ + EventProvider* event; + Message* initialmsg; + const MessageList* initialmsglist; + bool eventinit_done; + + public: + /** Constructor. + * @param protoevent Protocol event provider the event is an instance of. + */ + Event(EventProvider& protoeventprov) + : event(&protoeventprov) + , initialmsg(NULL) + , initialmsglist(NULL) + , eventinit_done(false) + { + } + + /** Constructor. + * @param protoevent Protocol event provider the event is an instance of. + * @param msg Message to include in this event by default. + */ + Event(EventProvider& protoeventprov, ClientProtocol::Message& msg) + : event(&protoeventprov) + , initialmsg(&msg) + , initialmsglist(NULL) + , eventinit_done(false) + { + } + + /** Set a single message as the initial message in the event. + * Modules may alter this later. + */ + void SetMessage(Message* msg) + { + initialmsg = msg; + initialmsglist = NULL; + } + + /** Set a list of messages as the initial messages in the event. + * Modules may alter this later. + */ + void SetMessageList(const MessageList& msglist) + { + initialmsg = NULL; + initialmsglist = &msglist; + } + + /** Get a list of messages to send to a user. + * The exact messages sent to a user are determined by the initial message(s) set and hooks. + * @param user User to get the messages for. + * @param messagelist List to fill in with messages to send to the user for the event + */ + void GetMessagesForUser(LocalUser* user, MessageList& messagelist); +}; + +/** Base class for message tag providers. + * All message tags belong to a message tag provider. Message tag providers can populate messages + * with tags before the message is sent and they have the job of determining whether a user should + * get a message tag or be allowed to send one. + */ +class ClientProtocol::MessageTagProvider : public Events::ModuleEventListener +{ + public: + /** Constructor. + * @param mod Module owning the provider. + */ + MessageTagProvider(Module* mod) + : Events::ModuleEventListener(mod, "event/messagetag") + { + } + + /** Called when a message is ready to be sent to give the tag provider a chance to add tags to the message. + * To add tags call Message::AddTag(). If the provided tag or tags have been added already elsewhere or if the + * provider doesn't want its tag(s) to be on the message, the implementation doesn't have to do anything special. + * The default implementation does nothing. + * @param msg Message to be populated with tags. + */ + virtual void OnClientProtocolPopulateTags(ClientProtocol::Message& msg) + { + } + + /** Called for each tag that the server receives from a client in a message. + * @param user User that sent the tag. + * @param tagname Name of the tag. + * @param value Value of the tag, empty string if the tag has no value. May be modified. + * @return MOD_RES_ALLOW to accept the tag with the value in 'value', MOD_RES_DENY to reject the tag and act as if it wasn't sent, + * MOD_RES_PASSTHRU to make no decision. If no hooks accept a tag, the tag is rejected. + * The default implementation returns MOD_RES_PASSTHRU. + */ + virtual ModResult OnClientProtocolProcessTag(LocalUser* user, const std::string& tagname, std::string& tagvalue) + { + return MOD_RES_PASSTHRU; + } + + /** Called whenever a user is about to receive a message that has a tag attached which is provided by this provider + * to determine whether or not the user should get the tag. + * @param user User in question. + * @param tagdata Tag in question. + * @return True if the tag should be sent to the user, false otherwise. + */ + virtual bool ShouldSendTag(LocalUser* user, const MessageTagData& tagdata) = 0; +}; + +/** Base class for client protocol event hooks. + * A protocol event hook is attached to a single event type. It has the ability to alter or block messages + * sent to users which belong to the event the hook is attached to. + */ +class ClientProtocol::EventHook : public Events::ModuleEventListener +{ + public: + static std::string GetEventName(const std::string& name) + { + return "event/protoevent_" + name; + } + + /** Constructor. + * @param mod Owner of the hook. + * @param name Name of the event to hook. + * @param priority Priority of the hook. Determines the order in which hooks for the same event get called. + * Optional. + */ + EventHook(Module* mod, const std::string& name, unsigned int priority = Events::ModuleEventListener::DefaultPriority) + : Events::ModuleEventListener(mod, GetEventName(name), priority) + { + } + + /** Called exactly once before an event is sent to any user. + * The default implementation doesn't do anything. + * @param ev Event being sent to one or more users. + */ + virtual void OnEventInit(const ClientProtocol::Event& ev) + { + } + + /** Called for each user that may receive the event. + * The hook may alter the messages sent to the user and decide whether further hooks should be executed. + * @param user User the message list is being prepared to be sent to. + * @param ev Event associated with the messages. + * @param messagelist List of messages to send to the user. The hook can alter this in any way it sees fit, for example it can replace messages, + * add new messages, etc. The list must not be empty when the method returns. + * @return MOD_RES_PASSTHRU to run hooks after the called hook or if no hooks are after the called hook, send the messages in messagelist to the user. + * MOD_RES_DENY to not send any messages to the user and to not run other hooks, + * MOD_RES_ALLOW to send the messages in messagelist to the user and to not run other hooks. + */ + virtual ModResult OnPreEventSend(LocalUser* user, const ClientProtocol::Event& ev, ClientProtocol::MessageList& messagelist) = 0; +}; + +/** Event provider for client protocol events. + * Protocol event hooks can be attached to the instances of these providers. The core has event + * providers for most common IRC events defined in RFC1459. + */ +class ClientProtocol::EventProvider : public Events::ModuleEventProvider +{ + public: + /** Constructor. + * @param Mod Module that owns the event provider. + * @param eventname Name of the event this provider is for, e.g. "JOIN", "PART", "NUMERIC". + * Should match command name if applicable. + */ + EventProvider(Module* Mod, const std::string& eventname) + : Events::ModuleEventProvider(Mod, ClientProtocol::EventHook::GetEventName(eventname)) + { + } +}; + +/** Commonly used client protocol events. + * Available via InspIRCd::GetRFCEvents(). + */ +struct ClientProtocol::RFCEvents +{ + EventProvider numeric; + EventProvider join; + EventProvider part; + EventProvider kick; + EventProvider quit; + EventProvider nick; + EventProvider mode; + EventProvider topic; + EventProvider privmsg; + EventProvider invite; + EventProvider ping; + EventProvider pong; + EventProvider error; + + RFCEvents() + : numeric(NULL, "NUMERIC") + , join(NULL, "JOIN") + , part(NULL, "PART") + , kick(NULL, "KICK") + , quit(NULL, "QUIT") + , nick(NULL, "NICK") + , mode(NULL, "MODE") + , topic(NULL, "TOPIC") + , privmsg(NULL, "PRIVMSG") + , invite(NULL, "INVITE") + , ping(NULL, "PING") + , pong(NULL, "PONG") + , error(NULL, "ERROR") + { + } +}; + +/** Base class for client protocol serializers. + * A serializer has to implement serialization and parsing of protocol messages to/from wire format. + */ +class CoreExport ClientProtocol::Serializer : public DataProvider +{ + Events::ModuleEventProvider evprov; + + /** Make a white list containing which tags a user should get. + * @param user User in question. + * @param tagmap Tag map that contains all possible tags. + * @return Whitelist of tags to send to the user. + */ + TagSelection MakeTagWhitelist(LocalUser* user, const TagMap& tagmap) const; + + public: + /** Constructor. + * @param mod Module owning the serializer. + * @param Name Name of the serializer, e.g. "rfc". + */ + Serializer(Module* mod, const char* Name); + + /** Handle a tag in a message being parsed. Call this method for each parsed tag. + * @param user User sending the tag. + * @param tagname Name of the tag. + * @param tagvalue Tag value, may be empty. + * @param tags TagMap to place the tag into, if it gets accepted. + * @return True if no error occured, false if the tag name is invalid or if this tag already exists. + */ + bool HandleTag(LocalUser* user, const std::string& tagname, std::string& tagvalue, TagMap& tags) const; + + /** Serialize a message for a user. + * @param user User to serialize the message for. + * @param msg Message to serialize. + * @return Raw serialized message, only containing the appropriate tags for the user. + * The reference is guaranteed to be valid as long as the Message object is alive and until the same + * Message is serialized for another user. + */ + const SerializedMessage& SerializeForUser(LocalUser* user, Message& msg); + + /** Serialize a high level protocol message into wire format. + * @param msg High level message to serialize. Contains all necessary information about the message, including all possible tags. + * @param tagwl Message tags to include in the serialized message. Tags attached to the message but not included in the whitelist must not + * appear in the output. This is because each user may get a different set of tags for the same message. + * @return Protocol message in wire format. Must contain message delimiter as well, if any (e.g. CRLF for RFC1459). + */ + virtual std::string Serialize(const Message& msg, const TagSelection& tagwl) const = 0; + + /** Parse a protocol message from wire format. + * @param user Source of the message. + * @param line Raw protocol message. + * @param parseoutput Output of the parser. + * @return True if the message was parsed successfully into parseoutput and should be processed, false to drop the message. + */ + virtual bool Parse(LocalUser* user, const std::string& line, ParseOutput& parseoutput) = 0; +}; + +inline ClientProtocol::MessageTagData::MessageTagData(MessageTagProvider* prov, const std::string& val, void* data) + : tagprov(prov) + , value(val) + , provdata(data) +{ +} |