#include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef SYSTEMD_FOUND # include #endif using namespace std::string_literals; static std::set kickable_errors{ "gone", "internal-server-error", "item-not-found", "jid-malformed", "recipient-unavailable", "redirect", "remote-server-not-found", "remote-server-timeout", "service-unavailable", "malformed-error" }; XmppComponent::XmppComponent(std::shared_ptr poller, const std::string& hostname, const std::string& secret): TCPSocketHandler(poller), ever_auth(false), first_connection_try(true), secret(secret), authenticated(false), doc_open(false), served_hostname(hostname), stanza_handlers{}, adhoc_commands_handler(this) { this->parser.add_stream_open_callback(std::bind(&XmppComponent::on_remote_stream_open, this, std::placeholders::_1)); this->parser.add_stanza_callback(std::bind(&XmppComponent::on_stanza, this, std::placeholders::_1)); this->parser.add_stream_close_callback(std::bind(&XmppComponent::on_remote_stream_close, this, std::placeholders::_1)); this->stanza_handlers.emplace("handshake", std::bind(&XmppComponent::handle_handshake, this,std::placeholders::_1)); this->stanza_handlers.emplace("error", std::bind(&XmppComponent::handle_error, this,std::placeholders::_1)); } void XmppComponent::start() { this->connect("127.0.0.1", Config::get("port", "5347"), false); } bool XmppComponent::is_document_open() const { return this->doc_open; } void XmppComponent::send_stanza(const Stanza& stanza) { std::string str = stanza.to_string(); log_debug("XMPP SENDING: " << str); this->send_data(std::move(str)); } void XmppComponent::on_connection_failed(const std::string& reason) { this->first_connection_try = false; log_error("Failed to connect to the XMPP server: " << reason); #ifdef SYSTEMD_FOUND sd_notifyf(0, "STATUS=Failed to connect to the XMPP server: %s", reason.data()); #endif } void XmppComponent::on_connected() { log_info("connected to XMPP server"); this->first_connection_try = true; XmlNode node("", nullptr); node.set_name("stream:stream"); node["xmlns"] = COMPONENT_NS; node["xmlns:stream"] = STREAM_NS; node["to"] = this->served_hostname; this->send_stanza(node); this->doc_open = true; // We may have some pending data to send: this happens when we try to send // some data before we are actually connected. We send that data right now, if any this->send_pending_data(); } void XmppComponent::on_connection_close(const std::string& error) { if (error.empty()) { log_info("XMPP server closed connection"); } else { log_info("XMPP server closed connection: " << error); } } void XmppComponent::parse_in_buffer(const size_t size) { if (!this->in_buf.empty()) { // This may happen if the parser could not allocate enough space for // us. We try to feed it the data that was read into our in_buf // instead. If this fails again we are in trouble. this->parser.feed(this->in_buf.data(), this->in_buf.size(), false); this->in_buf.clear(); } else { // Just tell the parser to parse the data that was placed into the // buffer it provided to us with GetBuffer this->parser.parse(size, false); } } void XmppComponent::on_remote_stream_open(const XmlNode& node) { log_debug("XMPP DOCUMENT OPEN: " << node.to_string()); this->stream_id = node.get_tag("id"); if (this->stream_id.empty()) { log_error("Error: no attribute 'id' found"); this->send_stream_error("bad-format", "missing 'id' attribute"); this->close_document(); return ; } // Try to authenticate char digest[HASH_LENGTH * 2 + 1]; sha1nfo sha1; sha1_init(&sha1); sha1_write(&sha1, this->stream_id.data(), this->stream_id.size()); sha1_write(&sha1, this->secret.data(), this->secret.size()); const uint8_t* result = sha1_result(&sha1); for (int i=0; i < HASH_LENGTH; i++) sprintf(digest + (i*2), "%02x", result[i]); digest[HASH_LENGTH * 2] = '\0'; Stanza handshake(COMPONENT_NS":handshake"); handshake.set_inner(digest); handshake.close(); this->send_stanza(handshake); } void XmppComponent::on_remote_stream_close(const XmlNode& node) { log_debug("XMPP DOCUMENT CLOSE " << node.to_string()); this->doc_open = false; } void XmppComponent::reset() { this->parser.reset(); } void XmppComponent::on_stanza(const Stanza& stanza) { log_debug("XMPP RECEIVING: " << stanza.to_string()); std::function handler; try { handler = this->stanza_handlers.at(stanza.get_name()); } catch (const std::out_of_range& exception) { log_warning("No handler for stanza of type " << stanza.get_name()); return; } handler(stanza); } void XmppComponent::send_stream_error(const std::string& name, const std::string& explanation) { XmlNode node("stream:error", nullptr); XmlNode error(name, nullptr); error["xmlns"] = STREAM_NS; if (!explanation.empty()) error.set_inner(explanation); error.close(); node.add_child(std::move(error)); node.close(); this->send_stanza(node); } void XmppComponent::send_stanza_error(const std::string& kind, const std::string& to, const std::string& from, const std::string& id, const std::string& error_type, const std::string& defined_condition, const std::string& text, const bool fulljid) { Stanza node(kind); if (!to.empty()) node["to"] = to; if (!from.empty()) { if (fulljid) node["from"] = from; else node["from"] = from + "@" + this->served_hostname; } if (!id.empty()) node["id"] = id; node["type"] = "error"; XmlNode error("error"); error["type"] = error_type; XmlNode inner_error(defined_condition); inner_error["xmlns"] = STANZA_NS; inner_error.close(); error.add_child(std::move(inner_error)); if (!text.empty()) { XmlNode text_node("text"); text_node["xmlns"] = STANZA_NS; text_node.set_inner(text); text_node.close(); error.add_child(std::move(text_node)); } error.close(); node.add_child(std::move(error)); node.close(); this->send_stanza(node); } void XmppComponent::close_document() { log_debug("XMPP SENDING: "); this->send_data(""); this->doc_open = false; } void XmppComponent::handle_handshake(const Stanza& stanza) { (void)stanza; this->authenticated = true; this->ever_auth = true; log_info("Authenticated with the XMPP server"); #ifdef SYSTEMD_FOUND sd_notify(0, "READY=1"); // Install an event that sends a keepalive to systemd. If biboumi crashes // or hangs for too long, systemd will restart it. uint64_t usec; if (sd_watchdog_enabled(0, &usec) > 0) { TimedEventsManager::instance().add_event(TimedEvent( std::chrono::duration_cast(std::chrono::microseconds(usec / 2)), []() { sd_notify(0, "WATCHDOG=1"); })); } #endif this->after_handshake(); } void XmppComponent::handle_error(const Stanza& stanza) { XmlNode* text = stanza.get_child("text", STREAMS_NS); std::string error_message("Unspecified error"); if (text) error_message = text->get_inner(); log_error("Stream error received from the XMPP server: " << error_message); #ifdef SYSTEMD_FOUND if (!this->ever_auth) sd_notifyf(0, "STATUS=Failed to authenticate to the XMPP server: %s", error_message.data()); #endif } void* XmppComponent::get_receive_buffer(const size_t size) const { return this->parser.get_buffer(size); } void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, const std::string& to, const std::string& type, const bool fulljid) { XmlNode node("message"); node["to"] = to; if (fulljid) node["from"] = from; else node["from"] = from + "@" + this->served_hostname; if (!type.empty()) node["type"] = type; XmlNode body_node("body"); body_node.set_inner(std::get<0>(body)); body_node.close(); node.add_child(std::move(body_node)); if (std::get<1>(body)) { XmlNode html("html"); html["xmlns"] = XHTMLIM_NS; // Pass the ownership of the pointer to this xmlnode html.add_child(std::get<1>(body).release()); html.close(); node.add_child(std::move(html)); } node.close(); this->send_stanza(node); } void XmppComponent::send_user_join(const std::string& from, const std::string& nick, const std::string& realjid, const std::string& affiliation, const std::string& role, const std::string& to, const bool self) { XmlNode node("presence"); node["to"] = to; node["from"] = from + "@" + this->served_hostname + "/" + nick; XmlNode x("x"); x["xmlns"] = MUC_USER_NS; XmlNode item("item"); if (!affiliation.empty()) item["affiliation"] = affiliation; if (!role.empty()) item["role"] = role; if (!realjid.empty()) { const std::string preped_jid = jidprep(realjid); if (!preped_jid.empty()) item["jid"] = preped_jid; } item.close(); x.add_child(std::move(item)); if (self) { XmlNode status("status"); status["code"] = "110"; status.close(); x.add_child(std::move(status)); } x.close(); node.add_child(std::move(x)); node.close(); this->send_stanza(node); } void XmppComponent::send_invalid_room_error(const std::string& muc_name, const std::string& nick, const std::string& to) { Stanza presence("presence"); if (!muc_name.empty()) presence["from"] = muc_name + "@" + this->served_hostname + "/" + nick; else presence["from"] = this->served_hostname; presence["to"] = to; presence["type"] = "error"; XmlNode x("x"); x["xmlns"] = MUC_NS; x.close(); presence.add_child(std::move(x)); XmlNode error("error"); error["by"] = muc_name + "@" + this->served_hostname; error["type"] = "cancel"; XmlNode item_not_found("item-not-found"); item_not_found["xmlns"] = STANZA_NS; item_not_found.close(); error.add_child(std::move(item_not_found)); XmlNode text("text"); text["xmlns"] = STANZA_NS; text["xml:lang"] = "en"; text.set_inner(muc_name + " is not a valid IRC channel name. A correct room jid is of the form: #%@" + this->served_hostname); text.close(); error.add_child(std::move(text)); error.close(); presence.add_child(std::move(error)); presence.close(); this->send_stanza(presence); } void XmppComponent::send_invalid_user_error(const std::string& user_name, const std::string& to) { Stanza message("message"); message["from"] = user_name + "@" + this->served_hostname; message["to"] = to; message["type"] = "error"; XmlNode x("x"); x["xmlns"] = MUC_NS; x.close(); message.add_child(std::move(x)); XmlNode error("error"); error["type"] = "cancel"; XmlNode item_not_found("item-not-found"); item_not_found["xmlns"] = STANZA_NS; item_not_found.close(); error.add_child(std::move(item_not_found)); XmlNode text("text"); text["xmlns"] = STANZA_NS; text["xml:lang"] = "en"; text.set_inner(user_name + " is not a valid IRC user name. A correct user jid is of the form: !@" + this->served_hostname); text.close(); error.add_child(std::move(text)); error.close(); message.add_child(std::move(error)); message.close(); this->send_stanza(message); } void XmppComponent::send_topic(const std::string& from, Xmpp::body&& topic, const std::string& to) { XmlNode message("message"); message["to"] = to; message["from"] = from + "@" + this->served_hostname; message["type"] = "groupchat"; XmlNode subject("subject"); subject.set_inner(std::get<0>(topic)); subject.close(); message.add_child(std::move(subject)); message.close(); this->send_stanza(message); } void XmppComponent::send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to) { Stanza message("message"); message["to"] = jid_to; if (!nick.empty()) message["from"] = muc_name + "@" + this->served_hostname + "/" + nick; else // Message from the room itself message["from"] = muc_name + "@" + this->served_hostname; message["type"] = "groupchat"; XmlNode body("body"); body.set_inner(std::get<0>(xmpp_body)); body.close(); message.add_child(std::move(body)); if (std::get<1>(xmpp_body)) { XmlNode html("html"); html["xmlns"] = XHTMLIM_NS; // Pass the ownership of the pointer to this xmlnode html.add_child(std::get<1>(xmpp_body).release()); html.close(); message.add_child(std::move(html)); } message.close(); this->send_stanza(message); } void XmppComponent::send_muc_leave(const std::string& muc_name, std::string&& nick, Xmpp::body&& message, const std::string& jid_to, const bool self) { Stanza presence("presence"); presence["to"] = jid_to; presence["from"] = muc_name + "@" + this->served_hostname + "/" + nick; presence["type"] = "unavailable"; const std::string message_str = std::get<0>(message); XmlNode x("x"); x["xmlns"] = MUC_USER_NS; if (self) { XmlNode status("status"); status["code"] = "110"; status.close(); x.add_child(std::move(status)); } x.close(); presence.add_child(std::move(x)); if (!message_str.empty()) { XmlNode status("status"); status.set_inner(message_str); status.close(); presence.add_child(std::move(status)); } presence.close(); this->send_stanza(presence); } void XmppComponent::send_nick_change(const std::string& muc_name, const std::string& old_nick, const std::string& new_nick, const std::string& affiliation, const std::string& role, const std::string& jid_to, const bool self) { Stanza presence("presence"); presence["to"] = jid_to; presence["from"] = muc_name + "@" + this->served_hostname + "/" + old_nick; presence["type"] = "unavailable"; XmlNode x("x"); x["xmlns"] = MUC_USER_NS; XmlNode item("item"); item["nick"] = new_nick; item.close(); x.add_child(std::move(item)); XmlNode status("status"); status["code"] = "303"; status.close(); x.add_child(std::move(status)); if (self) { XmlNode status2("status"); status2["code"] = "110"; status2.close(); x.add_child(std::move(status2)); } x.close(); presence.add_child(std::move(x)); presence.close(); this->send_stanza(presence); this->send_user_join(muc_name, new_nick, "", affiliation, role, jid_to, self); } void XmppComponent::kick_user(const std::string& muc_name, const std::string& target, const std::string& txt, const std::string& author, const std::string& jid_to) { Stanza presence("presence"); presence["from"] = muc_name + "@" + this->served_hostname + "/" + target; presence["to"] = jid_to; presence["type"] = "unavailable"; XmlNode x("x"); x["xmlns"] = MUC_USER_NS; XmlNode item("item"); item["affiliation"] = "none"; item["role"] = "none"; XmlNode actor("actor"); actor["nick"] = author; actor["jid"] = author; // backward compatibility with old clients actor.close(); item.add_child(std::move(actor)); XmlNode reason("reason"); reason.set_inner(txt); reason.close(); item.add_child(std::move(reason)); item.close(); x.add_child(std::move(item)); XmlNode status("status"); status["code"] = "307"; status.close(); x.add_child(std::move(status)); x.close(); presence.add_child(std::move(x)); presence.close(); this->send_stanza(presence); } void XmppComponent::send_presence_error(const std::string& muc_name, const std::string& nickname, const std::string& jid_to, const std::string& type, const std::string& condition, const std::string& error_code, const std::string& /* text */) { Stanza presence("presence"); presence["from"] = muc_name + "@" + this->served_hostname + "/" + nickname; presence["to"] = jid_to; presence["type"] = "error"; XmlNode x("x"); x["xmlns"] = MUC_NS; x.close(); presence.add_child(std::move(x)); XmlNode error("error"); error["by"] = muc_name + "@" + this->served_hostname; error["type"] = type; if (!error_code.empty()) error["code"] = error_code; XmlNode subnode(condition); subnode["xmlns"] = STANZA_NS; subnode.close(); error.add_child(std::move(subnode)); error.close(); presence.add_child(std::move(error)); presence.close(); this->send_stanza(presence); } void XmppComponent::send_affiliation_role_change(const std::string& muc_name, const std::string& target, const std::string& affiliation, const std::string& role, const std::string& jid_to) { Stanza presence("presence"); presence["from"] = muc_name + "@" + this->served_hostname + "/" + target; presence["to"] = jid_to; XmlNode x("x"); x["xmlns"] = MUC_USER_NS; XmlNode item("item"); item["affiliation"] = affiliation; item["role"] = role; item.close(); x.add_child(std::move(item)); x.close(); presence.add_child(std::move(x)); presence.close(); this->send_stanza(presence); } void XmppComponent::send_version(const std::string& id, const std::string& jid_to, const std::string& jid_from, const std::string& version) { Stanza iq("iq"); iq["type"] = "result"; iq["id"] = id; iq["to"] = jid_to; iq["from"] = jid_from; XmlNode query("query"); query["xmlns"] = VERSION_NS; if (version.empty()) { XmlNode name("name"); name.set_inner("biboumi"); name.close(); query.add_child(std::move(name)); XmlNode version("version"); version.set_inner(SOFTWARE_VERSION); version.close(); query.add_child(std::move(version)); XmlNode os("os"); os.set_inner(SYSTEM_NAME); os.close(); query.add_child(std::move(os)); } else { XmlNode name("name"); name.set_inner(version); name.close(); query.add_child(std::move(name)); } query.close(); iq.add_child(std::move(query)); iq.close(); 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) { Stanza iq("iq"); iq["type"] = "get"; iq["id"] = "version_"s + XmppComponent::next_id(); iq["from"] = from + "@" + this->served_hostname; iq["to"] = jid_to; XmlNode query("query"); query["xmlns"] = VERSION_NS; query.close(); iq.add_child(std::move(query)); iq.close(); this->send_stanza(iq); } void XmppComponent::send_iq_result(const std::string& id, const std::string& to_jid, const std::string& from_local_part) { Stanza iq("iq"); if (!from_local_part.empty()) iq["from"] = from_local_part + "@" + this->served_hostname; else iq["from"] = this->served_hostname; iq["to"] = to_jid; iq["id"] = id; iq["type"] = "result"; iq.close(); this->send_stanza(iq); } std::string XmppComponent::next_id() { char uuid_str[37]; uuid_t uuid; uuid_generate(uuid); uuid_unparse(uuid, uuid_str); return uuid_str; }