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;
+}
+
+
+