/* * purple - Handling of XEP-0047: In-Band Bytestreams. * * Purple is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * source distribution. * * This program 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; either version 2 of the License, or * (at your option) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA * */ #include #include #include "ibb.h" #define JABBER_IBB_SESSION_DEFAULT_BLOCK_SIZE 4096 static GHashTable *jabber_ibb_sessions = NULL; static GList *open_handlers = NULL; static JabberStream *jabber_ibb_session_get_js(JabberIBBSession *sess); JabberIBBSession * jabber_ibb_session_create(JabberStream *js, const gchar *sid, const gchar *who, gpointer user_data) { JabberIBBSession *sess = g_new0(JabberIBBSession, 1); sess->js = js; if (sid) { sess->sid = g_strdup(sid); } else { sess->sid = jabber_get_next_id(js); } sess->who = g_strdup(who); sess->block_size = JABBER_IBB_SESSION_DEFAULT_BLOCK_SIZE; sess->state = JABBER_IBB_SESSION_NOT_OPENED; sess->user_data = user_data; g_hash_table_insert(jabber_ibb_sessions, sess->sid, sess); return sess; } JabberIBBSession * jabber_ibb_session_create_from_xmlnode(JabberStream *js, const char *from, const char *id, PurpleXmlNode *open, gpointer user_data) { JabberIBBSession *sess = NULL; const gchar *sid = purple_xmlnode_get_attrib(open, "sid"); const gchar *block_size = purple_xmlnode_get_attrib(open, "block-size"); if (!open) { return NULL; } if (!sid || !block_size) { purple_debug_error("jabber", "IBB session open tag requires sid and block-size attributes\n"); return NULL; } sess = jabber_ibb_session_create(js, sid, from, user_data); sess->id = g_strdup(id); sess->block_size = atoi(block_size); /* if we create a session from an incoming request, it means the session is immediately open... */ sess->state = JABBER_IBB_SESSION_OPENED; return sess; } void jabber_ibb_session_destroy(JabberIBBSession *sess) { purple_debug_info("jabber", "IBB: destroying session %p %s\n", sess, sess->sid); if (jabber_ibb_session_get_state(sess) == JABBER_IBB_SESSION_OPENED) { jabber_ibb_session_close(sess); } if (sess->last_iq_id) { purple_debug_info("jabber", "IBB: removing callback for %s\n", sess->last_iq_id); jabber_iq_remove_callback_by_id(jabber_ibb_session_get_js(sess), sess->last_iq_id); g_free(sess->last_iq_id); sess->last_iq_id = NULL; } g_hash_table_remove(jabber_ibb_sessions, sess->sid); g_free(sess->id); g_free(sess->sid); g_free(sess->who); g_free(sess); } static const gchar * jabber_ibb_session_get_sid(const JabberIBBSession *sess) { return sess->sid; } static JabberStream * jabber_ibb_session_get_js(JabberIBBSession *sess) { return sess->js; } static const gchar * jabber_ibb_session_get_who(const JabberIBBSession *sess) { return sess->who; } static guint16 jabber_ibb_session_get_send_seq(const JabberIBBSession *sess) { return sess->send_seq; } static guint16 jabber_ibb_session_get_recv_seq(const JabberIBBSession *sess) { return sess->recv_seq; } JabberIBBSessionState jabber_ibb_session_get_state(const JabberIBBSession *sess) { return sess->state; } gsize jabber_ibb_session_get_block_size(const JabberIBBSession *sess) { return sess->block_size; } gsize jabber_ibb_session_get_max_data_size(const JabberIBBSession *sess) { return (gsize) floor((sess->block_size - 2) * (float) 3 / 4); } gpointer jabber_ibb_session_get_user_data(JabberIBBSession *sess) { return sess->user_data; } void jabber_ibb_session_set_opened_callback(JabberIBBSession *sess, JabberIBBOpenedCallback *cb) { sess->opened_cb = cb; } void jabber_ibb_session_set_data_sent_callback(JabberIBBSession *sess, JabberIBBSentCallback *cb) { sess->data_sent_cb = cb; } void jabber_ibb_session_set_closed_callback(JabberIBBSession *sess, JabberIBBClosedCallback *cb) { sess->closed_cb = cb; } void jabber_ibb_session_set_data_received_callback(JabberIBBSession *sess, JabberIBBDataCallback *cb) { sess->data_received_cb = cb; } void jabber_ibb_session_set_error_callback(JabberIBBSession *sess, JabberIBBErrorCallback *cb) { sess->error_cb = cb; } static void jabber_ibb_session_opened_cb(G_GNUC_UNUSED JabberStream *js, G_GNUC_UNUSED const char *from, JabberIqType type, G_GNUC_UNUSED const char *id, G_GNUC_UNUSED PurpleXmlNode *packet, gpointer data) { JabberIBBSession *sess = (JabberIBBSession *) data; if (type == JABBER_IQ_ERROR) { sess->state = JABBER_IBB_SESSION_ERROR; } else { sess->state = JABBER_IBB_SESSION_OPENED; } if (sess->opened_cb) { sess->opened_cb(sess); } } void jabber_ibb_session_open(JabberIBBSession *sess) { if (jabber_ibb_session_get_state(sess) != JABBER_IBB_SESSION_NOT_OPENED) { purple_debug_error("jabber", "jabber_ibb_session called on an already open stream\n"); } else { JabberIq *set = jabber_iq_new(sess->js, JABBER_IQ_SET); PurpleXmlNode *open = purple_xmlnode_new("open"); gchar block_size[10]; purple_xmlnode_set_attrib(set->node, "to", jabber_ibb_session_get_who(sess)); purple_xmlnode_set_namespace(open, NS_IBB); purple_xmlnode_set_attrib(open, "sid", jabber_ibb_session_get_sid(sess)); g_snprintf(block_size, sizeof(block_size), "%" G_GSIZE_FORMAT, jabber_ibb_session_get_block_size(sess)); purple_xmlnode_set_attrib(open, "block-size", block_size); purple_xmlnode_insert_child(set->node, open); jabber_iq_set_callback(set, jabber_ibb_session_opened_cb, sess); jabber_iq_send(set); } } void jabber_ibb_session_close(JabberIBBSession *sess) { JabberIBBSessionState state = jabber_ibb_session_get_state(sess); if (state != JABBER_IBB_SESSION_OPENED && state != JABBER_IBB_SESSION_ERROR) { purple_debug_error("jabber", "jabber_ibb_session_close called on a session that has not been" "opened\n"); } else { JabberIq *set = jabber_iq_new(jabber_ibb_session_get_js(sess), JABBER_IQ_SET); PurpleXmlNode *close = purple_xmlnode_new("close"); purple_xmlnode_set_attrib(set->node, "to", jabber_ibb_session_get_who(sess)); purple_xmlnode_set_namespace(close, NS_IBB); purple_xmlnode_set_attrib(close, "sid", jabber_ibb_session_get_sid(sess)); purple_xmlnode_insert_child(set->node, close); jabber_iq_send(set); sess->state = JABBER_IBB_SESSION_CLOSED; } } static void jabber_ibb_session_send_acknowledge_cb(G_GNUC_UNUSED JabberStream *js, G_GNUC_UNUSED const char *from, JabberIqType type, G_GNUC_UNUSED const char *id, G_GNUC_UNUSED PurpleXmlNode *packet, gpointer data) { JabberIBBSession *sess = (JabberIBBSession *) data; if (sess) { /* reset callback */ g_free(sess->last_iq_id); sess->last_iq_id = NULL; if (type == JABBER_IQ_ERROR) { jabber_ibb_session_close(sess); sess->state = JABBER_IBB_SESSION_ERROR; if (sess->error_cb) { sess->error_cb(sess); } } else { if (sess->data_sent_cb) { sess->data_sent_cb(sess); } } } else { /* the session has gone away, it was probably cancelled */ purple_debug_info("jabber", "got response from send data, but IBB session is no longer active\n"); } } void jabber_ibb_session_send_data(JabberIBBSession *sess, gconstpointer data, gsize size) { JabberIBBSessionState state = jabber_ibb_session_get_state(sess); purple_debug_info("jabber", "sending data block of %" G_GSIZE_FORMAT " bytes on IBB stream\n", size); if (state != JABBER_IBB_SESSION_OPENED) { purple_debug_error("jabber", "trying to send data on a non-open IBB session\n"); } else if (size > jabber_ibb_session_get_max_data_size(sess)) { purple_debug_error("jabber", "trying to send a too large packet in the IBB session\n"); } else { JabberIq *set = jabber_iq_new(jabber_ibb_session_get_js(sess), JABBER_IQ_SET); PurpleXmlNode *data_element = purple_xmlnode_new("data"); char *base64 = g_base64_encode(data, size); char seq[10]; g_snprintf(seq, sizeof(seq), "%u", jabber_ibb_session_get_send_seq(sess)); purple_xmlnode_set_attrib(set->node, "to", jabber_ibb_session_get_who(sess)); purple_xmlnode_set_namespace(data_element, NS_IBB); purple_xmlnode_set_attrib(data_element, "sid", jabber_ibb_session_get_sid(sess)); purple_xmlnode_set_attrib(data_element, "seq", seq); purple_xmlnode_insert_data(data_element, base64, -1); purple_xmlnode_insert_child(set->node, data_element); purple_debug_info("jabber", "IBB: setting send callback for session %p %s\n", sess, sess->sid); jabber_iq_set_callback(set, jabber_ibb_session_send_acknowledge_cb, sess); sess->last_iq_id = g_strdup(purple_xmlnode_get_attrib(set->node, "id")); purple_debug_info("jabber", "IBB: set sess->last_iq_id: %s\n", sess->last_iq_id); jabber_iq_send(set); g_free(base64); (sess->send_seq)++; } } static void jabber_ibb_send_error_response(JabberStream *js, const char *to, const char *id) { JabberIq *result = jabber_iq_new(js, JABBER_IQ_ERROR); PurpleXmlNode *error = purple_xmlnode_new("error"); PurpleXmlNode *item_not_found = purple_xmlnode_new("item-not-found"); purple_xmlnode_set_namespace(item_not_found, NS_XMPP_STANZAS); purple_xmlnode_set_attrib(error, "code", "440"); purple_xmlnode_set_attrib(error, "type", "cancel"); jabber_iq_set_id(result, id); purple_xmlnode_set_attrib(result->node, "to", to); purple_xmlnode_insert_child(error, item_not_found); purple_xmlnode_insert_child(result->node, error); jabber_iq_send(result); } /* Handle incoming packet. */ static void jabber_ibb_parse(JabberStream *js, const char *who, G_GNUC_UNUSED JabberIqType type, const char *id, PurpleXmlNode *child) { const char *name = child->name; gboolean data = purple_strequal(name, "data"); gboolean close = purple_strequal(name, "close"); gboolean open = purple_strequal(name, "open"); const gchar *sid = (data || close) ? purple_xmlnode_get_attrib(child, "sid") : NULL; JabberIBBSession *sess = sid ? g_hash_table_lookup(jabber_ibb_sessions, sid) : NULL; if (sess) { if (!purple_strequal(who, jabber_ibb_session_get_who(sess))) { /* the iq comes from a different JID than the remote JID of the session, ignore it */ purple_debug_error("jabber", "Got IBB iq from wrong JID, ignoring\n"); } else if (data) { const gchar *seq_attr = purple_xmlnode_get_attrib(child, "seq"); guint16 seq = (seq_attr ? atoi(seq_attr) : 0); /* reject the data, and set the session in error if we get an out-of-order packet */ if (seq_attr && seq == jabber_ibb_session_get_recv_seq(sess)) { /* sequence # is the expected... */ JabberIq *result = jabber_iq_new(js, JABBER_IQ_RESULT); jabber_iq_set_id(result, id); purple_xmlnode_set_attrib(result->node, "to", who); if (sess->data_received_cb) { gchar *base64 = purple_xmlnode_get_data(child); gsize size; gpointer rawdata = g_base64_decode(base64, &size); g_free(base64); if (rawdata) { purple_debug_info("jabber", "got %" G_GSIZE_FORMAT " bytes of data on IBB stream\n", size); /* we accept other clients to send up to block-size of _unencoded_ data, since there's been some confusions regarding the interpretation of this attribute (including previous versions of libpurple) */ if (size > jabber_ibb_session_get_block_size(sess)) { purple_debug_error("jabber", "IBB: received a too large packet\n"); if (sess->error_cb) sess->error_cb(sess); g_free(rawdata); return; } else { purple_debug_info("jabber", "calling IBB callback for received data\n"); sess->data_received_cb(sess, rawdata, size); } g_free(rawdata); } else { purple_debug_error("jabber", "IBB: invalid BASE64 data received\n"); if (sess->error_cb) sess->error_cb(sess); return; } } (sess->recv_seq)++; jabber_iq_send(result); } else { purple_debug_error("jabber", "Received an out-of-order/invalid IBB packet\n"); sess->state = JABBER_IBB_SESSION_ERROR; if (sess->error_cb) { sess->error_cb(sess); } } } else if (close) { sess->state = JABBER_IBB_SESSION_CLOSED; purple_debug_info("jabber", "IBB: received close\n"); if (sess->closed_cb) { purple_debug_info("jabber", "IBB: calling closed handler\n"); sess->closed_cb(sess); } } } else if (open) { JabberIq *result; const GList *iterator; /* run all open handlers registered until one returns true */ for (iterator = open_handlers ; iterator ; iterator = g_list_next(iterator)) { JabberIBBOpenHandler *handler = iterator->data; if (handler(js, who, id, child)) { result = jabber_iq_new(js, JABBER_IQ_RESULT); purple_xmlnode_set_attrib(result->node, "to", who); jabber_iq_set_id(result, id); jabber_iq_send(result); return; } } /* no open callback returned success, reject */ jabber_ibb_send_error_response(js, who, id); } else { /* send error reply */ jabber_ibb_send_error_response(js, who, id); } } void jabber_ibb_register_open_handler(JabberIBBOpenHandler *cb) { open_handlers = g_list_append(open_handlers, cb); } void jabber_ibb_unregister_open_handler(JabberIBBOpenHandler *cb) { open_handlers = g_list_remove(open_handlers, cb); } void jabber_ibb_init(void) { jabber_ibb_sessions = g_hash_table_new(g_str_hash, g_str_equal); jabber_add_feature(NS_IBB, NULL); jabber_iq_register_handler("close", NS_IBB, jabber_ibb_parse); jabber_iq_register_handler("data", NS_IBB, jabber_ibb_parse); jabber_iq_register_handler("open", NS_IBB, jabber_ibb_parse); } void jabber_ibb_uninit(void) { g_clear_pointer(&jabber_ibb_sessions, g_hash_table_destroy); g_clear_list(&open_handlers, NULL); }