Mercurial > pidgin.yaz
diff libpurple/protocols/jabber/ibb.c @ 23999:b4ec5481a67a
Implements file transfers using in-band bytestreams for XMPP
using XEP-0047
Refs #6183
author | Marcus Lundblad <ml@update.uu.se> |
---|---|
date | Sat, 06 Sep 2008 07:49:05 +0000 |
parents | |
children | dc01f9b0aaa3 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/ibb.c Sat Sep 06 07:49:05 2008 +0000 @@ -0,0 +1,514 @@ +/* + * 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 Library 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 02110-1301, USA + */ + +#include "ibb.h" +#include "debug.h" +#include "xmlnode.h" + +#include <glib.h> +#include <string.h> + +#define JABBER_IBB_SESSION_DEFAULT_BLOCK_SIZE 4096 + +static GHashTable *jabber_ibb_sessions = NULL; +static GList *open_handlers = NULL; + +JabberIBBSession * +jabber_ibb_session_create(JabberStream *js, const gchar *sid, const gchar *who, + gpointer user_data) +{ + JabberIBBSession *sess = g_new0(JabberIBBSession, 1); + + if (!sess) { + purple_debug_error("jabber", "Could not allocate IBB session object\n"); + return NULL; + } + + sess->js = js; + if (sid) { + sess->sid = g_strdup(sid); + } else { + sess->sid = g_strdup(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, xmlnode *packet, + gpointer user_data) +{ + JabberIBBSession *sess = NULL; + xmlnode *open = xmlnode_get_child_with_namespace(packet, "open", + XEP_0047_NAMESPACE); + const gchar *sid = xmlnode_get_attrib(open, "sid"); + const gchar *block_size = xmlnode_get_attrib(open, "block-size"); + gsize block_size_int; + + if (!open) { + return NULL; + } + + sess = g_new0(JabberIBBSession, 1); + + if (!sess) { + purple_debug_error("jabber", "Could not allocate IBB session object\n"); + return NULL; + } + + if (!sid || !block_size) { + purple_debug_error("jabber", + "IBB session open tag requires sid and block-size attributes\n"); + g_free(sess); + return NULL; + } + + block_size_int = atoi(block_size); + sess->js = js; + sess->sid = g_strdup(sid); + sess->id = g_strdup(xmlnode_get_attrib(packet, "id")); + sess->who = g_strdup(xmlnode_get_attrib(packet, "from")); + sess->block_size = block_size_int; + /* if we create a session from an incoming <open/> request, it means the + session is immediatly open... */ + sess->state = JABBER_IBB_SESSION_OPENED; + sess->user_data = user_data; + + g_hash_table_insert(jabber_ibb_sessions, sess->sid, sess); + + return sess; +} + +void +jabber_ibb_session_destroy(JabberIBBSession *sess) +{ + purple_debug_info("jabber", "IBB: destroying session %lx %s\n", sess, + sess->sid); + + if (jabber_ibb_session_get_state(sess) == JABBER_IBB_SESSION_OPENED) { + jabber_ibb_session_close(sess); + } + + purple_debug_info("jabber", "IBB: last_iq_id: %lx\n", sess->last_iq_id); + if (sess->last_iq_id) { + purple_debug_info("jabber", "IBB: removing callback for <iq/> %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->sid); + g_free(sess->who); + g_free(sess); +} + +const gchar * +jabber_ibb_session_get_sid(const JabberIBBSession *sess) +{ + return sess->sid; +} + +JabberStream * +jabber_ibb_session_get_js(JabberIBBSession *sess) +{ + return sess->js; +} + +const gchar * +jabber_ibb_session_get_who(const JabberIBBSession *sess) +{ + return sess->who; +} + +guint16 +jabber_ibb_session_get_send_seq(const JabberIBBSession *sess) +{ + return sess->send_seq; +} + +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; +} + +void +jabber_ibb_session_set_block_size(JabberIBBSession *sess, gsize size) +{ + if (jabber_ibb_session_get_state(sess) == JABBER_IBB_SESSION_NOT_OPENED) { + sess->block_size = size; + } else { + purple_debug_error("jabber", + "Can't set block size on an open IBB session\n"); + } +} + +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(JabberStream *js, xmlnode *packet, gpointer data) +{ + JabberIBBSession *sess = (JabberIBBSession *) data; + + 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); + xmlnode *open = xmlnode_new("open"); + gchar block_size[10]; + + xmlnode_set_attrib(set->node, "to", jabber_ibb_session_get_who(sess)); + xmlnode_set_namespace(open, XEP_0047_NAMESPACE); + xmlnode_set_attrib(open, "sid", jabber_ibb_session_get_sid(sess)); + g_snprintf(block_size, sizeof(block_size), "%ld", + jabber_ibb_session_get_block_size(sess)); + xmlnode_set_attrib(open, "block-size", block_size); + 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); + xmlnode *close = xmlnode_new("close"); + + xmlnode_set_attrib(set->node, "to", jabber_ibb_session_get_who(sess)); + xmlnode_set_namespace(close, XEP_0047_NAMESPACE); + xmlnode_set_attrib(close, "sid", jabber_ibb_session_get_sid(sess)); + xmlnode_insert_child(set->node, close); + jabber_iq_send(set); + sess->state = JABBER_IBB_SESSION_CLOSED; + } +} + +void +jabber_ibb_session_accept(JabberIBBSession *sess) +{ + JabberIq *result = jabber_iq_new(jabber_ibb_session_get_js(sess), + JABBER_IQ_RESULT); + + xmlnode_set_attrib(result->node, "to", jabber_ibb_session_get_who(sess)); + jabber_iq_set_id(result, sess->id); + jabber_iq_send(result); + sess->state = JABBER_IBB_SESSION_OPENED; +} + +static void +jabber_ibb_session_send_acknowledge_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + JabberIBBSession *sess = (JabberIBBSession *) data; + xmlnode *error = xmlnode_get_child(packet, "error"); + + if (sess) { + /* reset callback */ + if (sess->last_iq_id) { + g_free(sess->last_iq_id); + sess->last_iq_id = NULL; + } + + if (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, gpointer data, gsize size) +{ + JabberIBBSessionState state = jabber_ibb_session_get_state(sess); + + 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_block_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); + xmlnode *data_element = xmlnode_new("data"); + char *base64 = purple_base64_encode(data, size); + char seq[10]; + g_snprintf(seq, sizeof(seq), "%d", jabber_ibb_session_get_send_seq(sess)); + + xmlnode_set_attrib(set->node, "to", jabber_ibb_session_get_who(sess)); + xmlnode_set_namespace(data_element, XEP_0047_NAMESPACE); + xmlnode_set_attrib(data_element, "sid", jabber_ibb_session_get_sid(sess)); + xmlnode_set_attrib(data_element, "seq", seq); + xmlnode_insert_data(data_element, base64, strlen(base64)); + + xmlnode_insert_child(set->node, data_element); + + purple_debug_info("jabber", + "IBB: setting send <iq/> callback for session %lx %s\n", sess, + sess->sid); + jabber_iq_set_callback(set, jabber_ibb_session_send_acknowledge_cb, sess); + sess->last_iq_id = g_strdup(xmlnode_get_attrib(set->node, "id")); + purple_debug_info("jabber", "IBB: set sess->last_iq_id: %lx %lx\n", + sess->last_iq_id, xmlnode_get_attrib(set->node, "id")); + jabber_iq_send(set); + + g_free(base64); + (sess->send_seq)++; + } +} + +void +jabber_ibb_parse(JabberStream *js, xmlnode *packet) +{ + xmlnode *data = xmlnode_get_child_with_namespace(packet, "data", + XEP_0047_NAMESPACE); + xmlnode *close = xmlnode_get_child_with_namespace(packet, "close", + XEP_0047_NAMESPACE); + xmlnode *open = xmlnode_get_child_with_namespace(packet, "open", + XEP_0047_NAMESPACE); + const gchar *sid = + data ? xmlnode_get_attrib(data, "sid") : + close ? xmlnode_get_attrib(close, "sid") : NULL; + JabberIBBSession *sess = + sid ? g_hash_table_lookup(jabber_ibb_sessions, sid) : NULL; + const gchar *who = xmlnode_get_attrib(packet, "from"); + + if (sess) { + + if (strcmp(who, jabber_ibb_session_get_who(sess)) != 0) { + /* 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) { + guint16 seq = atoi(xmlnode_get_attrib(data, "seq")); + + /* reject the data, and set the session in error if we get an + out-of-order packet */ + if (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, xmlnode_get_attrib(packet, "id")); + xmlnode_set_attrib(result->node, "to", + xmlnode_get_attrib(packet, "from")); + + if (sess->data_received_cb) { + gchar *base64 = xmlnode_get_data(data); + gsize size; + gpointer rawdata = purple_base64_decode(base64, &size); + + g_free(base64); + + if (rawdata) { + if (size > jabber_ibb_session_get_block_size(sess)) { + purple_debug_error("jabber", + "IBB: received a too large packet\n"); + } else { + sess->data_received_cb(sess, rawdata, size); + } + g_free(rawdata); + } else { + purple_debug_error("jabber", + "IBB: invalid BASE64 data received\n"); + } + } + + (sess->recv_seq)++; + jabber_iq_send(result); + + } else { + purple_debug_error("jabber", + "Received an out-of-order 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 { + /* this should never happen */ + purple_debug_error("jabber", "Received bogus iq for IBB session\n"); + } + } 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 = (JabberIBBOpenHandler *) iterator->data; + + if (handler(js, packet)) { + result = jabber_iq_new(js, JABBER_IQ_RESULT); + xmlnode_set_attrib(result->node, "to", + xmlnode_get_attrib(packet, "from")); + jabber_iq_set_id(result, xmlnode_get_attrib(packet, "id")); + jabber_iq_send(result); + return; + } + } + } else { + /* send error reply */ + JabberIq *result = jabber_iq_new(js, JABBER_IQ_ERROR); + xmlnode *error = xmlnode_new("error"); + xmlnode *item_not_found = xmlnode_new("item-not-found"); + + xmlnode_set_namespace(item_not_found, + "urn:ietf:params:xml:ns:xmpp-stanzas"); + xmlnode_set_attrib(error, "code", "440"); + xmlnode_set_attrib(error, "type", "cancel"); + jabber_iq_set_id(result, xmlnode_get_attrib(packet, "id")); + xmlnode_set_attrib(result->node, "to", + xmlnode_get_attrib(packet, "from")); + xmlnode_insert_child(error, item_not_found); + xmlnode_insert_child(result->node, error); + + jabber_iq_send(result); + } +} + +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); +} + +void +jabber_ibb_uninit(void) +{ + g_hash_table_destroy(jabber_ibb_sessions); + g_list_free(open_handlers); + jabber_ibb_sessions = NULL; + open_handlers = NULL; +} + + +