From eb9a20187098185cc10ad192e91a90dbba12633a Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Tue, 27 May 2014 01:01:38 +0200 Subject: Implement the support for adhoc commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have two basic example commands. But it’s not entirely finished because there are some error checks that we don’t do. ref #2521 --- src/xmpp/adhoc_command.cpp | 82 +++++++++++++++++++++++++++++ src/xmpp/adhoc_command.hpp | 40 +++++++++++++++ src/xmpp/adhoc_commands_handler.cpp | 100 ++++++++++++++++++++++++++++++++++++ src/xmpp/adhoc_commands_handler.hpp | 56 ++++++++++++++++++++ src/xmpp/xmpp_component.cpp | 62 ++++++++++++++++++++-- src/xmpp/xmpp_component.hpp | 7 +++ src/xmpp/xmpp_stanza.cpp | 7 +++ src/xmpp/xmpp_stanza.hpp | 21 +++++++- 8 files changed, 370 insertions(+), 5 deletions(-) create mode 100644 src/xmpp/adhoc_command.cpp create mode 100644 src/xmpp/adhoc_command.hpp create mode 100644 src/xmpp/adhoc_commands_handler.cpp create mode 100644 src/xmpp/adhoc_commands_handler.hpp diff --git a/src/xmpp/adhoc_command.cpp b/src/xmpp/adhoc_command.cpp new file mode 100644 index 0000000..b880533 --- /dev/null +++ b/src/xmpp/adhoc_command.cpp @@ -0,0 +1,82 @@ +#include + +using namespace std::string_literals; + +AdhocCommand::AdhocCommand(std::vector&& callbacks, const std::string& name, const bool admin_only): + name(name), + callbacks(std::move(callbacks)), + admin_only(admin_only) +{ +} + +AdhocCommand::~AdhocCommand() +{ +} + +void PingStep1(AdhocSession&, XmlNode& command_node) +{ + XmlNode note("note"); + note["type"] = "info"; + note.set_inner("Pong"); + note.close(); + command_node.add_child(std::move(note)); +} + +void HelloStep1(AdhocSession&, XmlNode& command_node) +{ + XmlNode x("jabber:x:data:x"); + x["type"] = "form"; + XmlNode title("title"); + title.set_inner("Configure your name."); + title.close(); + x.add_child(std::move(title)); + XmlNode instructions("instructions"); + instructions.set_inner("Please provide your name."); + instructions.close(); + x.add_child(std::move(instructions)); + XmlNode name_field("field"); + name_field["var"] = "name"; + name_field["type"] = "text-single"; + name_field["label"] = "Your name"; + XmlNode required("required"); + required.close(); + name_field.add_child(std::move(required)); + name_field.close(); + x.add_child(std::move(name_field)); + x.close(); + command_node.add_child(std::move(x)); +} + +void HelloStep2(AdhocSession& session, XmlNode& command_node) +{ + // Find out if the name was provided in the form. + XmlNode* x = command_node.get_child("x", "jabber:x:data"); + if (x) + { + XmlNode* name_field = nullptr; + for (XmlNode* field: x->get_children("field", "jabber:x:data")) + if (field->get_tag("var") == "name") + { + name_field = field; + break; + } + if (name_field) + { + XmlNode* value = name_field->get_child("value", "jabber:x:data"); + if (value) + { + XmlNode note("note"); + note["type"] = "info"; + note.set_inner("Hello "s + value->get_inner() + "!"s); + note.close(); + command_node.delete_all_children(); + command_node.add_child(std::move(note)); + return; + } + } + } + // TODO insert an error telling the name value is missing. Also it's + // useless to terminate it, since this step is the last of the command + // anyway. But this is for the example. + session.terminate(); +} diff --git a/src/xmpp/adhoc_command.hpp b/src/xmpp/adhoc_command.hpp new file mode 100644 index 0000000..59a3cd6 --- /dev/null +++ b/src/xmpp/adhoc_command.hpp @@ -0,0 +1,40 @@ +#ifndef ADHOC_COMMAND_HPP +# define ADHOC_COMMAND_HPP + +/** + * Describe an ad-hoc command. + * + * Can only have zero or one step for now. When execution is requested, it + * can return a result immediately, or provide a form to be filled, and + * provide a result once the filled form is received. + */ + +#include + +#include +#include + +class AdhocCommand +{ + friend class AdhocSession; +public: + AdhocCommand(std::vector&& callback, const std::string& name, const bool admin_only); + ~AdhocCommand(); + + const std::string name; + +private: + /** + * A command may have one or more steps. Each step is a different + * callback, inserting things into a XmlNode and calling + * methods of an AdhocSession. + */ + std::vector callbacks; + const bool admin_only; +}; + +void PingStep1(AdhocSession& session, XmlNode& command_node); +void HelloStep1(AdhocSession& session, XmlNode& command_node); +void HelloStep2(AdhocSession& session, XmlNode& command_node); + +#endif // ADHOC_COMMAND_HPP diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp new file mode 100644 index 0000000..a669ca1 --- /dev/null +++ b/src/xmpp/adhoc_commands_handler.cpp @@ -0,0 +1,100 @@ +#include +#include + +#include + +#include + +AdhocCommandsHandler::AdhocCommandsHandler(): + commands{ + {"ping", AdhocCommand({&PingStep1}, "Do a ping", false)}, + {"hello", AdhocCommand({&HelloStep1, &HelloStep2}, "Receive a custom greeting", false)} + } +{ +} + +AdhocCommandsHandler::~AdhocCommandsHandler() +{ +} + +const std::map& AdhocCommandsHandler::get_commands() const +{ + return this->commands; +} + +XmlNode&& AdhocCommandsHandler::handle_request(const std::string& executor_jid, XmlNode command_node) +{ + // TODO check the type of action. Currently it assumes it is always + // 'execute'. + std::string action = command_node.get_tag("action"); + if (action.empty()) + action = "execute"; + command_node.del_tag("action"); + + const std::string node = command_node.get_tag("node"); + auto command_it = this->commands.find(node); + if (command_it == this->commands.end()) + { + XmlNode error(ADHOC_NS":error"); + error["type"] = "cancel"; + XmlNode condition(STANZA_NS":item-not-found"); + condition.close(); + error.add_child(std::move(condition)); + error.close(); + command_node.add_child(std::move(error)); + } + else + { + std::string sessionid = command_node.get_tag("sessionid"); + if (sessionid.empty()) + { // create a new session, with a new id + sessionid = XmppComponent::next_id(); + command_node["sessionid"] = sessionid; + this->sessions.emplace(std::piecewise_construct, + std::forward_as_tuple(sessionid, executor_jid), + std::forward_as_tuple(command_it->second, executor_jid)); + // TODO add a timed event to have an expiration date that deletes + // this session. We could have a nasty client starting commands + // but never finishing the last step, and that would fill the map + // with dummy sessions. + } + auto session_it = this->sessions.find(std::make_pair(sessionid, executor_jid)); + if (session_it == this->sessions.end()) + { + XmlNode error(ADHOC_NS":error"); + error["type"] = "modify"; + XmlNode condition(STANZA_NS":bad-request"); + condition.close(); + error.add_child(std::move(condition)); + error.close(); + command_node.add_child(std::move(error)); + } + else + { + // execute the step + AdhocSession& session = session_it->second; + const AdhocStep& step = session.get_next_step(); + step(session, command_node); + if (session.remaining_steps() == 0 || + session.is_terminated()) + { + this->sessions.erase(session_it); + command_node["status"] = "completed"; + } + else + { + command_node["status"] = "executing"; + XmlNode actions("actions"); + XmlNode next("next"); + next.close(); + actions.add_child(std::move(next)); + actions.close(); + command_node.add_child(std::move(actions)); + } + } + } + // TODO remove that once we make sure so session can stay there for ever, + // by mistake. + log_debug("Number of existing sessions: " << this->sessions.size()); + return std::move(command_node); +} diff --git a/src/xmpp/adhoc_commands_handler.hpp b/src/xmpp/adhoc_commands_handler.hpp new file mode 100644 index 0000000..6e00188 --- /dev/null +++ b/src/xmpp/adhoc_commands_handler.hpp @@ -0,0 +1,56 @@ +#ifndef ADHOC_COMMANDS_HANDLER_HPP +# define ADHOC_COMMANDS_HANDLER_HPP + +/** + * Manage a list of available AdhocCommands and the list of ongoing + * AdhocCommandSessions. + */ + +#include +#include + +#include +#include +#include + +class AdhocCommandsHandler +{ +public: + explicit AdhocCommandsHandler(); + ~AdhocCommandsHandler(); + /** + * Returns the list of available commands. + */ + const std::map& get_commands() const; + /** + * Find the requested command, create a new session or use an existing + * one, and process the request (provide a new form, an error, or a + * result). + * + * Returns a (moved) XmlNode that will be inserted in the iq response. It + * should be a node containing one or more useful children. If + * it contains an node, the iq response will have an error type. + * + * Takes a copy of the node so we can actually edit it and use + * it as our return value. + */ + XmlNode&& handle_request(const std::string& executor_jid, XmlNode command_node); +private: + /** + * The list of all available commands. + */ + const std::map commands; + /** + * The list of all currently on-going commands. + * + * Of the form: {{session_id, owner_jid}, session}. + */ + std::map, AdhocSession> sessions; + + AdhocCommandsHandler(const AdhocCommandsHandler&) = delete; + AdhocCommandsHandler(AdhocCommandsHandler&&) = delete; + AdhocCommandsHandler& operator=(const AdhocCommandsHandler&) = delete; + AdhocCommandsHandler& operator=(AdhocCommandsHandler&&) = delete; +}; + +#endif // ADHOC_COMMANDS_HANDLER_HPP diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp index 9ac05ac..4c62aa7 100644 --- a/src/xmpp/xmpp_component.cpp +++ b/src/xmpp/xmpp_component.cpp @@ -447,6 +447,22 @@ void XmppComponent::handle_iq(const Stanza& stanza) } } } + else if ((query = stanza.get_child("command", ADHOC_NS))) + { + Stanza response("iq"); + response["to"] = from; + response["from"] = this->served_hostname; + response["id"] = id; + XmlNode inner_node = this->adhoc_commands_handler.handle_request(from, *query); + if (inner_node.get_child("error", ADHOC_NS)) + response["type"] = "error"; + else + response["type"] = "result"; + response.add_child(std::move(inner_node)); + response.close(); + this->send_stanza(response); + stanza_error.disable(); + } } else if (type == "get") { @@ -454,8 +470,22 @@ void XmppComponent::handle_iq(const Stanza& stanza) if ((query = stanza.get_child("query", DISCO_INFO_NS))) { // Disco info if (to_str == this->served_hostname) - { // On the gateway itself - this->send_self_disco_info(id, from); + { + const std::string node = query->get_tag("node"); + if (node.empty()) + { + // On the gateway itself + this->send_self_disco_info(id, from); + stanza_error.disable(); + } + } + } + else if ((query = stanza.get_child("query", DISCO_ITEMS_NS))) + { + const std::string node = query->get_tag("node"); + if (node == ADHOC_NS) + { + this->send_adhoc_commands_list(id, from); stanza_error.disable(); } } @@ -848,8 +878,7 @@ void XmppComponent::send_self_disco_info(const std::string& id, const std::strin identity["name"] = "Biboumi XMPP-IRC gateway"; identity.close(); query.add_child(std::move(identity)); - for (const std::string& ns: {"http://jabber.org/protocol/disco#info", - "http://jabber.org/protocol/muc"}) + for (const std::string& ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS}) { XmlNode feature("feature"); feature["var"] = ns; @@ -862,6 +891,31 @@ void XmppComponent::send_self_disco_info(const std::string& id, const std::strin this->send_stanza(iq); } +void XmppComponent::send_adhoc_commands_list(const std::string& id, const std::string& requester_jid) +{ + Stanza iq("iq"); + iq["type"] = "result"; + iq["id"] = id; + iq["to"] = requester_jid; + iq["from"] = this->served_hostname; + XmlNode query("query"); + query["xmlns"] = DISCO_ITEMS_NS; + query["node"] = ADHOC_NS; + for (const auto& kv: this->adhoc_commands_handler.get_commands()) + { + XmlNode item("item"); + item["jid"] = this->served_hostname; + item["node"] = kv.first; + item["name"] = kv.second.name; + item.close(); + query.add_child(std::move(item)); + } + query.close(); + iq.add_child(std::move(query)); + iq.close(); + this->send_stanza(iq); +} + void XmppComponent::send_iq_version_request(const std::string& from, const std::string& jid_to) { diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp index d934704..b234d76 100644 --- a/src/xmpp/xmpp_component.hpp +++ b/src/xmpp/xmpp_component.hpp @@ -1,6 +1,7 @@ #ifndef XMPP_COMPONENT_INCLUDED # define XMPP_COMPONENT_INCLUDED +#include #include #include #include @@ -177,6 +178,11 @@ public: * Send a result IQ with the gateway disco informations. */ void send_self_disco_info(const std::string& id, const std::string& jid_to); + /** + * Send the list of all available ad-hoc commands to that JID. The list is + * different depending on what JID made the request. + */ + void send_adhoc_commands_list(const std::string& id, const std::string& requester_jid); /** * Send an iq version request */ @@ -231,6 +237,7 @@ private: static unsigned long current_id; + AdhocCommandsHandler adhoc_commands_handler; XmppComponent(const XmppComponent&) = delete; XmppComponent(XmppComponent&&) = delete; XmppComponent& operator=(const XmppComponent&) = delete; diff --git a/src/xmpp/xmpp_stanza.cpp b/src/xmpp/xmpp_stanza.cpp index be9f8ae..c964c64 100644 --- a/src/xmpp/xmpp_stanza.cpp +++ b/src/xmpp/xmpp_stanza.cpp @@ -243,6 +243,13 @@ const std::string XmlNode::get_tag(const std::string& name) const } } +bool XmlNode::del_tag(const std::string& name) +{ + if (this->attributes.erase(name) != 0) + return true; + return false; +} + std::string& XmlNode::operator[](const std::string& name) { return this->attributes[name]; diff --git a/src/xmpp/xmpp_stanza.hpp b/src/xmpp/xmpp_stanza.hpp index cc8d53a..e55d555 100644 --- a/src/xmpp/xmpp_stanza.hpp +++ b/src/xmpp/xmpp_stanza.hpp @@ -34,6 +34,21 @@ public: { node.parent = nullptr; } + /** + * The copy constructor do not copy the children or parent attributes. The + * copied node is identical to the original except that it is not attached + * to any other node. + */ + XmlNode(const XmlNode& node): + name(node.name), + parent(nullptr), + closed(node.closed), + attributes(node.attributes), + children{}, + inner(node.inner), + tail(node.tail) + { + } ~XmlNode(); @@ -103,6 +118,11 @@ public: * node as no such attribute. */ const std::string get_tag(const std::string& name) const; + /** + * Remove the attribute of the node. Does nothing if that attribute is not + * present. Returns true if the tag was removed, false if it was absent. + */ + bool del_tag(const std::string& name); /** * Use this to set an attribute's value, like node["id"] = "12"; */ @@ -117,7 +137,6 @@ private: std::string inner; std::string tail; - XmlNode(const XmlNode&) = delete; XmlNode& operator=(const XmlNode&) = delete; XmlNode& operator=(XmlNode&&) = delete; }; -- cgit v1.2.3