diff libpurple/protocols/jabber/si.c @ 25129: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 e21c79681c96
children be762644486f
line wrap: on
line diff
--- a/libpurple/protocols/jabber/si.c	Fri Sep 05 12:07:37 2008 +0000
+++ b/libpurple/protocols/jabber/si.c	Sat Sep 06 07:49:05 2008 +0000
@@ -35,6 +35,7 @@
 #include "jabber.h"
 #include "iq.h"
 #include "si.h"
+#include "ibb.h"
 
 #define STREAMHOST_CONNECT_TIMEOUT 15
 
@@ -64,8 +65,14 @@
 	size_t rxlen;
 	gsize rxmaxlen;
 	int local_streamhost_fd;
+	
+	JabberIBBSession *ibb_session;
+	FILE *fp;
 } JabberSIXfer;
 
+/* some forward declarations */
+static void jabber_si_xfer_ibb_send_init(JabberStream *js, PurpleXfer *xfer);
+
 static PurpleXfer*
 jabber_si_xfer_find(JabberStream *js, const char *sid, const char *from)
 {
@@ -204,8 +211,25 @@
 
 		jabber_iq_send(iq);
 
-		purple_xfer_cancel_local(xfer);
-
+		/* if IBB is available, revert to that before giving up... */
+		if (jsx->stream_method & STREAM_METHOD_IBB) {
+			/* if we are the initializer, init IBB */
+			purple_debug_info("jabber", 
+				"jabber_si_bytestreams_attempt_connect: "
+				"no streamhosts found, trying IBB\n");
+			/* if we are the sender, open an IBB session. But not if we already
+			  done it, since we could have received the error <iq/> from the
+			  receiver already... */
+			if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND
+				&& !jsx->ibb_session) {
+				jabber_si_xfer_ibb_send_init(jsx->js, xfer);
+			}
+			/* if we are the receiver, just wait for IBB open, callback is
+			  already set up... */
+		} else {
+			purple_xfer_cancel_local(xfer);
+		}
+		
 		return;
 	}
 
@@ -666,8 +690,29 @@
 	jsx = xfer->data;
 
 	if(!(type = xmlnode_get_attrib(packet, "type")) || strcmp(type, "result")) {
-		if (type && !strcmp(type, "error"))
-			purple_xfer_cancel_remote(xfer);
+	  purple_debug_info("jabber", 
+			    "jabber_si_xfer_connect_proxy_cb: type = %s\n",
+			    type);
+		if (type && !strcmp(type, "error")) {
+			/* if IBB is available, open IBB session */
+			purple_debug_info("jabber", 
+				"jabber_si_xfer_connect_proxy_cb: got error, method: %d\n",
+				jsx->stream_method);
+			if (jsx->stream_method & STREAM_METHOD_IBB) {
+				purple_debug_info("jabber", "IBB is possible, try it\n");
+				/* if we are the sender and haven't already opened an IBB
+				  session, do so now (we might already have failed to open
+				  the bytestream proxy ourselves when receiving this <iq/> */
+				if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND
+					&& !jsx->ibb_session) {
+					jabber_si_xfer_ibb_send_init(js, xfer);
+				}
+				/* if we are receiver, just wait for IBB open stanza, callback
+				  is already set up */
+			} else {
+				purple_xfer_cancel_remote(xfer);
+			}
+		}
 		return;
 	}
 
@@ -694,8 +739,19 @@
 			purple_debug_info("jabber", "Got local SOCKS5 streamhost-used.\n");
 			purple_xfer_start(xfer, xfer->fd, NULL, -1);
 		} else {
-			purple_debug_info("jabber", "streamhost-used does not match any proxy that was offered to target\n");
-			purple_xfer_cancel_local(xfer);
+			/* if available, try to revert to IBB... */
+			if (jsx->stream_method & STREAM_METHOD_IBB) {
+				purple_debug_info("jabber", 
+					"jabber_si_connect_proxy_cb: trying to revert to IBB\n");
+				if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND) {
+					jabber_si_xfer_ibb_send_init(jsx->js, xfer);
+				}
+				/* if we are the receiver, we are already set up...*/
+			} else {
+				purple_debug_info("jabber", 
+					"streamhost-used does not match any proxy that was offered to target\n");
+				purple_xfer_cancel_local(xfer);
+			}
 		}
 		g_free(my_jid);
 		return;
@@ -822,8 +878,23 @@
 	/* We have no way of transferring, cancel the transfer */
 	if (streamhost_count == 0) {
 		jabber_iq_free(iq);
-		/* We should probably notify the target, but this really shouldn't ever happen */
-		purple_xfer_cancel_local(xfer);
+		
+		/* if available, revert to IBB */
+		if (jsx->stream_method & STREAM_METHOD_IBB) {
+			purple_debug_info("jabber",
+				"jabber_si_xfer_bytestreams_listen_cb: trying to revert to IBB\n");
+			if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND) {
+				/* if we are the sender, init the IBB session... */
+				jabber_si_xfer_ibb_send_init(jsx->js, xfer);
+			}
+			/* if we are the receiver, we should just wait... the IBB open
+			  handler has already been set up... */
+		} else {
+			/* We should probably notify the target, 
+			  but this really shouldn't ever happen */
+			purple_xfer_cancel_local(xfer);
+		}
+		
 		return;
 	}
 
@@ -853,11 +924,200 @@
 
 }
 
+/* forward declare some functions here... */
+static void jabber_si_xfer_cancel_recv(PurpleXfer *xfer);
+static void jabber_si_xfer_cancel_send(PurpleXfer *xfer);
+static void jabber_si_xfer_free(PurpleXfer *xfer);
+
+static void
+jabber_si_xfer_ibb_error_cb(JabberIBBSession *sess)
+{
+	PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess);
+	JabberStream *js = jabber_ibb_session_get_js(sess);
+	PurpleConnection *gc = js->gc;
+	PurpleAccount *account = purple_connection_get_account(gc);
+	
+	purple_debug_error("jabber", "an error occured during IBB file transfer\n");
+	purple_xfer_error(purple_xfer_get_type(xfer), account,
+		jabber_ibb_session_get_who(sess), 
+			_("An error occured on the in-band bytestream transfer\n"));
+	purple_xfer_end(xfer);
+}
+
+static void
+jabber_si_xfer_ibb_closed_cb(JabberIBBSession *sess)
+{
+	PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess);
+	JabberStream *js = jabber_ibb_session_get_js(sess);
+	PurpleConnection *gc = js->gc;
+	PurpleAccount *account = purple_connection_get_account(gc);
+	
+	purple_debug_info("jabber", "the remote user closed the transfer\n");
+	if (purple_xfer_get_bytes_remaining(xfer) > 0) {
+		purple_xfer_error(purple_xfer_get_type(xfer), account,
+			jabber_ibb_session_get_who(sess), _("Transfer was closed."));
+		purple_xfer_end(xfer);
+	} else {
+		purple_xfer_set_completed(xfer, TRUE);
+	}
+}
+
+static void
+jabber_si_xfer_ibb_recv_data_cb(JabberIBBSession *sess, gpointer data,
+	gsize size)
+{
+	PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess);
+	JabberSIXfer *jsx = (JabberSIXfer *) xfer->data;
+	
+	if (size <= purple_xfer_get_bytes_remaining(xfer)) {
+		fwrite(data, size, 1, jsx->fp);
+		purple_xfer_set_bytes_sent(xfer, purple_xfer_get_bytes_sent(xfer) + size);
+		purple_xfer_update_progress(xfer);
+		
+		if (purple_xfer_get_bytes_remaining(xfer) == 0) {
+			purple_xfer_set_completed(xfer, TRUE);
+		}
+	} else {
+		/* trying to write past size of file transfers negotiated size,
+		  reject transfer to protect against malicious behaviour */
+		purple_debug_error("jabber", 
+			"IBB file transfer, trying to write past end of file\n");
+		jabber_si_xfer_cancel_recv(xfer);
+	}
+	
+}
+
+static gboolean
+jabber_si_xfer_ibb_open_cb(JabberStream *js, xmlnode *packet)
+{
+	const gchar *who = xmlnode_get_attrib(packet, "from");
+	xmlnode *open = xmlnode_get_child(packet, "open");
+	const gchar *sid = xmlnode_get_attrib(open, "sid");
+	PurpleXfer *xfer = jabber_si_xfer_find(js, sid, who);
+	JabberSIXfer *jsx = (JabberSIXfer *) xfer->data;
+	JabberIBBSession *sess = 
+		jabber_ibb_session_create_from_xmlnode(js, packet, xfer);
+	
+	if (sess) {
+		/* setup callbacks here...*/
+		jabber_ibb_session_set_data_received_callback(sess,
+			jabber_si_xfer_ibb_recv_data_cb);
+		jabber_ibb_session_set_closed_callback(sess, 
+			jabber_si_xfer_ibb_closed_cb);
+		jabber_ibb_session_set_error_callback(sess,
+			jabber_si_xfer_ibb_error_cb);
+		
+		/* open the file to write to */
+		jsx->fp = g_fopen(purple_xfer_get_local_filename(xfer), "w");
+		
+		jsx->ibb_session = sess;
+		
+		/* start the transfer */
+		purple_xfer_start(xfer, 0, NULL, 0);
+		return TRUE;
+	} else {
+		/* failed to create IBB session */
+		purple_debug_error("jabber", "failed to create IBB session\n");
+		jabber_si_xfer_cancel_recv(xfer);
+		return FALSE;
+	}
+}
+															
+static void
+jabber_si_xfer_ibb_send_data(JabberIBBSession *sess)
+{
+	PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess);
+	JabberSIXfer *jsx = (JabberSIXfer *) xfer->data;
+	gsize remaining = purple_xfer_get_bytes_remaining(xfer);
+	gsize packet_size = remaining < jabber_ibb_session_get_block_size(sess) ?
+		remaining : jabber_ibb_session_get_block_size(sess);
+	gpointer data = g_malloc(packet_size);
+	int res;
+	
+	purple_debug_info("jabber", "IBB: about to read %d bytes from file %lx\n",
+		packet_size, jsx->fp);
+	res = fread(data, packet_size, 1, jsx->fp);
+	
+	if (res == 1) {
+		jabber_ibb_session_send_data(sess, data, packet_size);
+		purple_xfer_set_bytes_sent(xfer, 
+			purple_xfer_get_bytes_sent(xfer) + packet_size);
+		purple_xfer_update_progress(xfer);
+	} else {
+		purple_debug_error("jabber", 
+			"jabber_si_xfer_ibb_opened_cb: error reading from file\n");
+		jabber_si_xfer_cancel_send(xfer);
+	}
+}
+
+static void
+jabber_si_xfer_ibb_sent_cb(JabberIBBSession *sess)
+{
+	PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess);
+	gsize remaining = purple_xfer_get_bytes_remaining(xfer);
+	
+	if (remaining == 0) {
+		/* close the session */
+		jabber_ibb_session_close(sess);
+		purple_xfer_set_completed(xfer, TRUE);
+	} else {
+		/* send more... */
+		jabber_si_xfer_ibb_send_data(sess);
+	}
+}
+
+static void
+jabber_si_xfer_ibb_opened_cb(JabberIBBSession *sess)
+{
+	PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess);
+	JabberSIXfer *jsx = (JabberSIXfer *) xfer->data;
+	
+	purple_xfer_start(xfer, 0, NULL, 0);
+	purple_xfer_set_bytes_sent(xfer, 0);
+	purple_xfer_update_progress(xfer);
+	jsx->fp = g_fopen(purple_xfer_get_local_filename(xfer), "r");
+	jabber_si_xfer_ibb_send_data(sess);
+}
+
+static void
+jabber_si_xfer_ibb_send_init(JabberStream *js, PurpleXfer *xfer)
+{
+	JabberSIXfer *jsx = (JabberSIXfer *) xfer->data;
+	
+	purple_xfer_ref(xfer);
+	
+	jsx->ibb_session = jabber_ibb_session_create(js, jsx->stream_id,
+		purple_xfer_get_remote_user(xfer), xfer);
+	
+	if (jsx->ibb_session) {
+		/* should set callbacks here... */
+		jabber_ibb_session_set_opened_callback(jsx->ibb_session,
+			jabber_si_xfer_ibb_opened_cb);
+		jabber_ibb_session_set_data_sent_callback(jsx->ibb_session,
+			jabber_si_xfer_ibb_sent_cb);
+		jabber_ibb_session_set_closed_callback(jsx->ibb_session,
+			jabber_si_xfer_ibb_closed_cb);
+		jabber_ibb_session_set_error_callback(jsx->ibb_session,
+			jabber_si_xfer_ibb_error_cb);
+		
+		/* open the IBB session */
+		jabber_ibb_session_open(jsx->ibb_session);
+		
+	} else {
+		/* failed to create IBB session */
+		purple_xfer_unref(xfer);
+		purple_debug_error("jabber", 
+			"failed to initiate IBB session for file transfer\n");
+		jabber_si_xfer_cancel_send(xfer);
+	}
+}
+	
 static void jabber_si_xfer_send_method_cb(JabberStream *js, xmlnode *packet,
 		gpointer data)
 {
 	PurpleXfer *xfer = data;
 	xmlnode *si, *feature, *x, *field, *value;
+	gboolean found_method = FALSE;
 
 	if(!(si = xmlnode_get_child_with_namespace(packet, "si", "http://jabber.org/protocol/si"))) {
 		purple_xfer_cancel_remote(xfer);
@@ -876,20 +1136,33 @@
 
 	for(field = xmlnode_get_child(x, "field"); field; field = xmlnode_get_next_twin(field)) {
 		const char *var = xmlnode_get_attrib(field, "var");
-
+		JabberSIXfer *jsx = (JabberSIXfer *) xfer->data;
+		
 		if(var && !strcmp(var, "stream-method")) {
 			if((value = xmlnode_get_child(field, "value"))) {
 				char *val = xmlnode_get_data(value);
 				if(val && !strcmp(val, "http://jabber.org/protocol/bytestreams")) {
 					jabber_si_xfer_bytestreams_send_init(xfer);
-					g_free(val);
-					return;
+					jsx->stream_method |= STREAM_METHOD_BYTESTREAMS;
+					found_method = TRUE;
+				} else if (val && !strcmp(val, XEP_0047_NAMESPACE)) {
+					jsx->stream_method |= STREAM_METHOD_IBB;
+					if (!found_method) {
+						/* we haven't tried to init a bytestream session, yet
+						  start IBB right away... */
+						jabber_si_xfer_ibb_send_init(js, xfer);
+						found_method = TRUE;
+					}
 				}
 				g_free(val);
 			}
 		}
 	}
-	purple_xfer_cancel_remote(xfer);
+	
+	if (!found_method) {
+		purple_xfer_cancel_remote(xfer);
+	}
+	
 }
 
 static void jabber_si_xfer_send_request(PurpleXfer *xfer)
@@ -929,11 +1202,9 @@
 	option = xmlnode_new_child(field, "option");
 	value = xmlnode_new_child(option, "value");
 	xmlnode_insert_data(value, "http://jabber.org/protocol/bytestreams", -1);
-	/*
 	option = xmlnode_new_child(field, "option");
 	value = xmlnode_new_child(option, "value");
 	xmlnode_insert_data(value, "http://jabber.org/protocol/ibb", -1);
-	*/
 
 	jabber_iq_set_callback(iq, jabber_si_xfer_send_method_cb, xfer);
 
@@ -967,6 +1238,16 @@
 		g_list_free(jsx->streamhosts);
 	}
 
+	if (jsx->ibb_session) {
+		purple_debug_info("jabber", 
+			"jabber_si_xfer_free: destroying IBB session\n");
+		jabber_ibb_session_destroy(jsx->ibb_session);
+	}
+	
+	if (jsx->fp) {
+		fclose(jsx->fp);
+	}
+	
 	g_free(jsx->stream_id);
 	g_free(jsx->iq_id);
 	/* XXX: free other stuff */
@@ -979,6 +1260,12 @@
 
 static void jabber_si_xfer_cancel_send(PurpleXfer *xfer)
 {
+	JabberSIXfer *jsx = (JabberSIXfer *) xfer->data;
+	
+	/* if there is an IBB session active, send close on that */
+	if (jsx->ibb_session) {
+		jabber_ibb_session_close(jsx->ibb_session);
+	}
 	jabber_si_xfer_free(xfer);
 	purple_debug(PURPLE_DEBUG_INFO, "jabber", "in jabber_si_xfer_cancel_send\n");
 }
@@ -993,6 +1280,11 @@
 
 static void jabber_si_xfer_cancel_recv(PurpleXfer *xfer)
 {
+	JabberSIXfer *jsx = (JabberSIXfer *) xfer->data;
+	/* if there is an IBB session active, send close */
+	if (jsx->ibb_session) {
+		jabber_ibb_session_close(jsx->ibb_session);
+	}
 	jabber_si_xfer_free(xfer);
 	purple_debug(PURPLE_DEBUG_INFO, "jabber", "in jabber_si_xfer_cancel_recv\n");
 }
@@ -1007,9 +1299,16 @@
 static void jabber_si_xfer_send_disco_cb(JabberStream *js, const char *who,
 		JabberCapabilities capabilities, gpointer data)
 {
-	PurpleXfer *xfer = data;
+	PurpleXfer *xfer = (PurpleXfer *) data;
+	JabberSIXfer *jsx = (JabberSIXfer *) xfer->data;
+	
+	if (capabilities & JABBER_CAP_IBB) {
+		purple_debug_info("jabber", 
+			"jabber_si_xfer_send_disco_cb: remote JID supports IBB\n");
+		jsx->stream_method |= STREAM_METHOD_IBB;
+	}
 
-	if(capabilities & JABBER_CAP_SI_FILE_XFER) {
+	if (capabilities & JABBER_CAP_SI_FILE_XFER) {
 		jabber_si_xfer_send_request(xfer);
 	} else {
 		char *msg = g_strdup_printf(_("Unable to send file to %s, user does not support file transfers"), who);
@@ -1136,18 +1435,20 @@
 		x = xmlnode_new_child(feature, "x");
 		xmlnode_set_namespace(x, "jabber:x:data");
 		xmlnode_set_attrib(x, "type", "submit");
-
 		field = xmlnode_new_child(x, "field");
 		xmlnode_set_attrib(field, "var", "stream-method");
-
-		value = xmlnode_new_child(field, "value");
-		if(jsx->stream_method & STREAM_METHOD_BYTESTREAMS)
+					
+		/* we should maybe "remember" if bytestreams has failed before (in the
+			same session) with this JID, and only present IBB as an option to
+			avoid unnessesary timeout */
+		if (jsx->stream_method & STREAM_METHOD_BYTESTREAMS) {
+			value = xmlnode_new_child(field, "value");
 			xmlnode_insert_data(value, "http://jabber.org/protocol/bytestreams", -1);
-		/*
-		else if(jsx->stream_method & STREAM_METHOD_IBB)
-		xmlnode_insert_data(value, "http://jabber.org/protocol/ibb", -1);
-		*/
-
+		} else if(jsx->stream_method & STREAM_METHOD_IBB) {
+			value = xmlnode_new_child(field, "value");
+			xmlnode_insert_data(value, "http://jabber.org/protocol/ibb", -1);
+		}
+							  
 		jabber_iq_send(iq);
 	}
 }
@@ -1167,6 +1468,9 @@
 		xfer->data = jsx = g_new0(JabberSIXfer, 1);
 		jsx->js = js;
 		jsx->local_streamhost_fd = -1;
+		
+		jsx->ibb_session = NULL;
+		jsx->fp = NULL;
 
 		purple_xfer_set_init_fnc(xfer, jabber_si_xfer_init);
 		purple_xfer_set_cancel_send_fnc(xfer, jabber_si_xfer_cancel_send);
@@ -1238,6 +1542,8 @@
 
 	jsx = g_new0(JabberSIXfer, 1);
 	jsx->local_streamhost_fd = -1;
+				   
+	jsx->ibb_session = NULL;
 
 	for(field = xmlnode_get_child(x, "field"); field; field = xmlnode_get_next_twin(field)) {
 		const char *var = xmlnode_get_attrib(field, "var");
@@ -1249,10 +1555,8 @@
 					if((val = xmlnode_get_data(value))) {
 						if(!strcmp(val, "http://jabber.org/protocol/bytestreams")) {
 							jsx->stream_method |= STREAM_METHOD_BYTESTREAMS;
-							/*
 						} else if(!strcmp(val, "http://jabber.org/protocol/ibb")) {
 							jsx->stream_method |= STREAM_METHOD_IBB;
-							*/
 						}
 						g_free(val);
 					}
@@ -1290,4 +1594,15 @@
 	}
 }
 
+void
+jabber_si_init(void)
+{
+	jabber_ibb_register_open_handler(jabber_si_xfer_ibb_open_cb);
+}
 
+void
+jabber_si_uninit(void)
+{
+	jabber_ibb_unregister_open_handler(jabber_si_xfer_ibb_open_cb);
+}
+