changeset 21603:a4b6854737d5

Implement more of XEP-0065 to support sending files through a proxy. To avoid adding strings this close to a release, it only supports using a proxy that is discovered from the server, but we'll include an account option to manually specify a ft proxy in the next release. Lots of this is based on a patch from galt - Fixes #3730, #116, #1768
author Daniel Atallah <daniel.atallah@gmail.com>
date Wed, 21 Nov 2007 05:22:39 +0000
parents 53fee49ce1c5
children 2c45b94ab722
files ChangeLog libpurple/protocols/jabber/disco.c libpurple/protocols/jabber/iq.c libpurple/protocols/jabber/jabber.c libpurple/protocols/jabber/jabber.h libpurple/protocols/jabber/libxmpp.c libpurple/protocols/jabber/si.c
diffstat 7 files changed, 375 insertions(+), 78 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog	Wed Nov 21 02:53:27 2007 +0000
+++ b/ChangeLog	Wed Nov 21 05:22:39 2007 +0000
@@ -16,6 +16,11 @@
 	  implementation.
 	* XMPP password changes that return errors no longer cause the saved
 	  password to be changed.
+	* XMPP file transfer support has been enhanced to support sending
+	  files through a proxy when the server supports discovering a
+	  a bytestream proxy.  This should make file transfers much more
+	  reliable.  The next release will add support for manually specifying
+	  a proxy when the server doesn't advertise one.
 
 	Pidgin:
 	* If a plugin says it can't be unloaded, we now display an error and
--- a/libpurple/protocols/jabber/disco.c	Wed Nov 21 02:53:27 2007 +0000
+++ b/libpurple/protocols/jabber/disco.c	Wed Nov 21 05:22:39 2007 +0000
@@ -44,6 +44,37 @@
 	xmlnode_set_attrib(feature, "var", x); \
 }
 
+static void
+jabber_disco_bytestream_server_cb(JabberStream *js, xmlnode *packet, gpointer data) {
+	JabberBytestreamsStreamhost *sh = data;
+	const char *from = xmlnode_get_attrib(packet, "from");
+	xmlnode *query = xmlnode_get_child_with_namespace(packet, "query",
+		"http://jabber.org/protocol/bytestreams");
+
+	if (from && !strcmp(from, sh->jid) && query != NULL) {
+		xmlnode *sh_node = xmlnode_get_child(query, "streamhost");
+		if (sh_node) {
+			const char *jid = xmlnode_get_attrib(sh_node, "jid");
+			const char *port = xmlnode_get_attrib(sh_node, "port");
+
+
+			if (jid == NULL || strcmp(jid, from) != 0)
+				purple_debug_error("jabber", "Invalid jid(%s) for bytestream.\n",
+						   jid ? jid : "(null)");
+
+			sh->host = g_strdup(xmlnode_get_attrib(sh_node, "host"));
+			sh->zeroconf = g_strdup(xmlnode_get_attrib(sh_node, "zeroconf"));
+			if (port != NULL)
+				sh->port = atoi(port);
+		}
+	}
+
+	purple_debug_info("jabber", "Discovered bytestream proxy server: "
+			  "jid='%s' host='%s' port='%d' zeroconf='%s'\n",
+			   from ? from : "", sh->host ? sh->host : "",
+			   sh->port, sh->zeroconf ? sh->zeroconf : "");
+}
+
 
 void jabber_disco_info_parse(JabberStream *js, xmlnode *packet) {
 	const char *from = xmlnode_get_attrib(packet, "from");
@@ -191,10 +222,26 @@
 				if(!strcmp(category, "conference") && !strcmp(type, "text")) {
 					/* we found a groupchat or MUC server, add it to the list */
 					/* XXX: actually check for protocol/muc or gc-1.0 support */
-					js->chat_servers = g_list_append(js->chat_servers, g_strdup(from));
+					js->chat_servers = g_list_prepend(js->chat_servers, g_strdup(from));
 				} else if(!strcmp(category, "directory") && !strcmp(type, "user")) {
 					/* we found a JUD */
-					js->user_directories = g_list_append(js->user_directories, g_strdup(from));
+					js->user_directories = g_list_prepend(js->user_directories, g_strdup(from));
+				} else if(!strcmp(category, "proxy") && !strcmp(type, "bytestreams")) {
+					/* This is a bytestream proxy */
+					JabberIq *iq;
+					JabberBytestreamsStreamhost *sh;
+
+					purple_debug_info("jabber", "Found bytestream proxy server: %s\n", from);
+
+					sh = g_new0(JabberBytestreamsStreamhost, 1);
+					sh->jid = g_strdup(from);
+					js->bs_proxies = g_list_prepend(js->bs_proxies, sh);
+
+					iq = jabber_iq_new_query(js, JABBER_IQ_GET,
+								 "http://jabber.org/protocol/bytestreams");
+					xmlnode_set_attrib(iq->node, "to", sh->jid);
+					jabber_iq_set_callback(iq, jabber_disco_bytestream_server_cb, sh);
+					jabber_iq_send(iq);
 				}
 
 			} else if(!strcmp(child->name, "feature")) {
@@ -344,8 +391,8 @@
 		g_free(js->server_name);
 		js->server_name = g_strdup(name);
 		if (!strcmp(name, "Google Talk")) {
-		  purple_debug_info("jabber", "Google Talk!\n");
-		  js->googletalk = TRUE;
+			purple_debug_info("jabber", "Google Talk!\n");
+			js->googletalk = TRUE;
 		}
 	}
 
@@ -422,8 +469,7 @@
 	jabber_iq_set_callback(iq, jabber_disco_server_items_result_cb, NULL);
 	jabber_iq_send(iq);
 
-	iq = jabber_iq_new_query(js, JABBER_IQ_GET,
-		                 "http://jabber.org/protocol/disco#info");
+	iq = jabber_iq_new_query(js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#info");
 	xmlnode_set_attrib(iq->node, "to", js->user->domain);
 	jabber_iq_set_callback(iq, jabber_disco_server_info_result_cb, NULL);
 	jabber_iq_send(iq);
--- a/libpurple/protocols/jabber/iq.c	Wed Nov 21 02:53:27 2007 +0000
+++ b/libpurple/protocols/jabber/iq.c	Wed Nov 21 05:22:39 2007 +0000
@@ -399,5 +399,6 @@
 void jabber_iq_uninit(void)
 {
 	g_hash_table_destroy(iq_handlers);
+	iq_handlers = NULL;
 }
 
--- a/libpurple/protocols/jabber/jabber.c	Wed Nov 21 02:53:27 2007 +0000
+++ b/libpurple/protocols/jabber/jabber.c	Wed Nov 21 05:22:39 2007 +0000
@@ -1234,20 +1234,31 @@
 		g_hash_table_destroy(js->buddies);
 	if(js->chats)
 		g_hash_table_destroy(js->chats);
+
 	while(js->chat_servers) {
 		g_free(js->chat_servers->data);
 		js->chat_servers = g_list_delete_link(js->chat_servers, js->chat_servers);
 	}
+
 	while(js->user_directories) {
 		g_free(js->user_directories->data);
 		js->user_directories = g_list_delete_link(js->user_directories, js->user_directories);
 	}
-	if(js->stream_id)
-		g_free(js->stream_id);
+
+	while(js->bs_proxies) {
+		JabberBytestreamsStreamhost *sh = js->bs_proxies->data;
+		g_free(sh->jid);
+		g_free(sh->host);
+		g_free(sh->zeroconf);
+		g_free(sh);
+		js->bs_proxies = g_list_delete_link(js->bs_proxies, js->bs_proxies);
+	}
+
+	g_free(js->stream_id);
 	if(js->user)
 		jabber_id_free(js->user);
-	if(js->avatar_hash)
-		g_free(js->avatar_hash);
+	g_free(js->avatar_hash);
+
 	purple_circ_buffer_destroy(js->write_buffer);
 	if(js->writeh)
 		purple_input_remove(js->writeh);
@@ -1256,11 +1267,9 @@
 		sasl_dispose(&js->sasl);
 	if(js->sasl_mechs)
 		g_string_free(js->sasl_mechs, TRUE);
-	if(js->sasl_cb)
-		g_free(js->sasl_cb);
+	g_free(js->sasl_cb);
 #endif
-	if(js->serverFQDN)
-		g_free(js->serverFQDN);
+	g_free(js->serverFQDN);
 	while(js->commands) {
 		JabberAdHocCommands *cmd = js->commands->data;
 		g_free(cmd->jid);
@@ -1272,21 +1281,14 @@
 	g_free(js->server_name);
 	g_free(js->gmail_last_time);
 	g_free(js->gmail_last_tid);
-	if(js->old_msg)
-		g_free(js->old_msg);
-	if(js->old_avatarhash)
-		g_free(js->old_avatarhash);
-	if(js->old_artist)
-		g_free(js->old_artist);
-	if(js->old_title)
-		g_free(js->old_title);
-	if(js->old_source)
-		g_free(js->old_source);
-	if(js->old_uri)
-		g_free(js->old_uri);
-	if(js->old_track)
-		g_free(js->old_track);
-	
+	g_free(js->old_msg);
+	g_free(js->old_avatarhash);
+	g_free(js->old_artist);
+	g_free(js->old_title);
+	g_free(js->old_source);
+	g_free(js->old_uri);
+	g_free(js->old_track);
+
 	g_free(js);
 
 	gc->proto_data = NULL;
--- a/libpurple/protocols/jabber/jabber.h	Wed Nov 21 02:53:27 2007 +0000
+++ b/libpurple/protocols/jabber/jabber.h	Wed Nov 21 05:22:39 2007 +0000
@@ -117,7 +117,7 @@
 	GHashTable *disco_callbacks;
 	int next_id;
 
-
+	GList *bs_proxies;
 	GList *oob_file_transfers;
 	GList *file_transfers;
 
@@ -146,7 +146,7 @@
 	char *gmail_last_time;
 	char *gmail_last_tid;
 
-    char *serverFQDN;
+	char *serverFQDN;
 
 	/* OK, this stays at the end of the struct, so plugins can depend
 	 * on the rest of the stuff being in the right place
@@ -202,6 +202,13 @@
 	JabberFeatureEnabled *is_enabled;
 } JabberFeature;
 
+typedef struct _JabberBytestreamsStreamhost {
+	char *jid;
+	char *host;
+	int port;
+	char *zeroconf;
+} JabberBytestreamsStreamhost;
+
 /* what kind of additional features as returned from disco#info are supported? */
 extern GList *jabber_features;
 
--- a/libpurple/protocols/jabber/libxmpp.c	Wed Nov 21 02:53:27 2007 +0000
+++ b/libpurple/protocols/jabber/libxmpp.c	Wed Nov 21 05:22:39 2007 +0000
@@ -233,7 +233,15 @@
 	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options,
 											   option);
 	
-	
+#if 0 /* TODO: Enable this when we're string unfrozen */
+	option = purple_account_option_string_new(_("File transfer proxies"),
+						  "ft_proxies",
+						/* TODO: Is this an acceptable default? */
+						  "proxy.jabber.org:7777");
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options,
+						  option);
+#endif
+
 	jabber_init_plugin(plugin);
 	
 	purple_prefs_remove("/plugins/prpl/jabber");
--- a/libpurple/protocols/jabber/si.c	Wed Nov 21 02:53:27 2007 +0000
+++ b/libpurple/protocols/jabber/si.c	Wed Nov 21 05:22:39 2007 +0000
@@ -36,19 +36,14 @@
 #include "iq.h"
 #include "si.h"
 
-#include "si.h"
-
-struct bytestreams_streamhost {
-	char *jid;
-	char *host;
-	int port;
-};
+#define STREAMHOST_CONNECT_TIMEOUT 15
 
 typedef struct _JabberSIXfer {
 	JabberStream *js;
 
 	PurpleProxyConnectData *connect_data;
 	PurpleNetworkListenData *listen_data;
+	guint connect_timeout;
 
 	gboolean accepted;
 
@@ -99,39 +94,82 @@
 	JabberSIXfer *jsx = xfer->data;
 	JabberIq *iq;
 	xmlnode *query, *su;
-	struct bytestreams_streamhost *streamhost = jsx->streamhosts->data;
+	JabberBytestreamsStreamhost *streamhost = jsx->streamhosts->data;
 
 	purple_proxy_info_destroy(jsx->gpi);
+	jsx->gpi = NULL;
 	jsx->connect_data = NULL;
 
+	if (jsx->connect_timeout > 0)
+		purple_timeout_remove(jsx->connect_timeout);
+	jsx->connect_timeout = 0;
+
 	if(source < 0) {
 		purple_debug_warning("jabber",
 				"si connection failed, jid was %s, host was %s, error was %s\n",
-				streamhost->jid, streamhost->host, error_message);
+				streamhost->jid, streamhost->host,
+				error_message ? error_message : "(null)");
 		jsx->streamhosts = g_list_remove(jsx->streamhosts, streamhost);
 		g_free(streamhost->jid);
 		g_free(streamhost->host);
+		g_free(streamhost->zeroconf);
 		g_free(streamhost);
 		jabber_si_bytestreams_attempt_connect(xfer);
 		return;
 	}
 
-	iq = jabber_iq_new_query(jsx->js, JABBER_IQ_RESULT, "http://jabber.org/protocol/bytestreams");
-	xmlnode_set_attrib(iq->node, "to", xfer->who);
-	jabber_iq_set_id(iq, jsx->iq_id);
-	query = xmlnode_get_child(iq->node, "query");
-	su = xmlnode_new_child(query, "streamhost-used");
-	xmlnode_set_attrib(su, "jid", streamhost->jid);
+	/* unknown file transfer type is assumed to be RECEIVE */
+	if(xfer->type == PURPLE_XFER_SEND)
+	{
+		xmlnode *activate;
+		iq = jabber_iq_new_query(jsx->js, JABBER_IQ_SET, "http://jabber.org/protocol/bytestreams");
+		xmlnode_set_attrib(iq->node, "to", streamhost->jid);
+		query = xmlnode_get_child(iq->node, "query");
+		xmlnode_set_attrib(query, "sid", jsx->stream_id);
+		activate = xmlnode_new_child(query, "activate");
+		xmlnode_insert_data(activate, xfer->who, -1);
+
+		/* TODO: We need to wait for an activation result before starting */
+	}
+	else
+	{
+		iq = jabber_iq_new_query(jsx->js, JABBER_IQ_RESULT, "http://jabber.org/protocol/bytestreams");
+		xmlnode_set_attrib(iq->node, "to", xfer->who);
+		jabber_iq_set_id(iq, jsx->iq_id);
+		query = xmlnode_get_child(iq->node, "query");
+		su = xmlnode_new_child(query, "streamhost-used");
+		xmlnode_set_attrib(su, "jid", streamhost->jid);
+	}
 
 	jabber_iq_send(iq);
 
 	purple_xfer_start(xfer, source, NULL, -1);
 }
 
+static gboolean
+connect_timeout_cb(gpointer data)
+{
+	PurpleXfer *xfer = data;
+	JabberSIXfer *jsx = xfer->data;
+
+	purple_debug_info("jabber", "Streamhost connection timeout of %d seconds exceeded.\n", STREAMHOST_CONNECT_TIMEOUT);
+
+	jsx->connect_timeout = 0;
+
+	if (jsx->connect_data != NULL)
+		purple_proxy_connect_cancel(jsx->connect_data);
+	jsx->connect_data = NULL;
+
+	/* Trigger the connect error manually */
+	jabber_si_bytestreams_connect_cb(xfer, -1, "Timeout Exceeded.");
+
+	return FALSE;
+}
+
 static void jabber_si_bytestreams_attempt_connect(PurpleXfer *xfer)
 {
 	JabberSIXfer *jsx = xfer->data;
-	struct bytestreams_streamhost *streamhost;
+	JabberBytestreamsStreamhost *streamhost;
 	char *dstaddr, *p;
 	int i;
 	unsigned char hashval[20];
@@ -160,18 +198,28 @@
 
 	streamhost = jsx->streamhosts->data;
 
+	jsx->connect_data = NULL;
+	if (jsx->gpi != NULL)
+		purple_proxy_info_destroy(jsx->gpi);
+	jsx->gpi = NULL;
+
 	dstjid = jabber_id_new(xfer->who);
 
-	if(dstjid != NULL) {
+	/* TODO: Deal with zeroconf */
+
+	if(dstjid != NULL && streamhost->host && streamhost->port > 0) {
 		jsx->gpi = purple_proxy_info_new();
 		purple_proxy_info_set_type(jsx->gpi, PURPLE_PROXY_SOCKS5);
 		purple_proxy_info_set_host(jsx->gpi, streamhost->host);
 		purple_proxy_info_set_port(jsx->gpi, streamhost->port);
 
-
-
-		dstaddr = g_strdup_printf("%s%s@%s/%s%s@%s/%s", jsx->stream_id, dstjid->node, dstjid->domain, dstjid->resource, jsx->js->user->node,
-				jsx->js->user->domain, jsx->js->user->resource);
+		/* unknown file transfer type is assumed to be RECEIVE */
+		if(xfer->type == PURPLE_XFER_SEND)
+			dstaddr = g_strdup_printf("%s%s@%s/%s%s@%s/%s", jsx->stream_id, jsx->js->user->node, jsx->js->user->domain,
+				jsx->js->user->resource, dstjid->node, dstjid->domain, dstjid->resource);
+		else
+			dstaddr = g_strdup_printf("%s%s@%s/%s%s@%s/%s", jsx->stream_id, dstjid->node, dstjid->domain, dstjid->resource,
+				jsx->js->user->node, jsx->js->user->domain, jsx->js->user->resource);
 
 		purple_cipher_digest_region("sha1", (guchar *)dstaddr, strlen(dstaddr),
 				sizeof(hashval), hashval, NULL);
@@ -186,6 +234,11 @@
 				jabber_si_bytestreams_connect_cb, xfer);
 		g_free(dstaddr);
 
+		/* When selecting a streamhost, timeout after STREAMHOST_CONNECT_TIMEOUT seconds, otherwise it takes forever */
+		if (xfer->type != PURPLE_XFER_SEND && jsx->connect_data != NULL)
+			jsx->connect_timeout = purple_timeout_add_seconds(
+				STREAMHOST_CONNECT_TIMEOUT, connect_timeout_cb, xfer);
+
 		jabber_id_free(dstjid);
 	}
 
@@ -194,6 +247,7 @@
 		jsx->streamhosts = g_list_remove(jsx->streamhosts, streamhost);
 		g_free(streamhost->jid);
 		g_free(streamhost->host);
+		g_free(streamhost->zeroconf);
 		g_free(streamhost);
 		jabber_si_bytestreams_attempt_connect(xfer);
 	}
@@ -232,17 +286,19 @@
 
 	for(streamhost = xmlnode_get_child(query, "streamhost"); streamhost;
 			streamhost = xmlnode_get_next_twin(streamhost)) {
-		const char *jid, *host, *port;
-		int portnum;
+		const char *jid, *host = NULL, *port, *zeroconf;
+		int portnum = 0;
 
 		if((jid = xmlnode_get_attrib(streamhost, "jid")) &&
-				(host = xmlnode_get_attrib(streamhost, "host")) &&
+				((zeroconf = xmlnode_get_attrib(streamhost, "zeroconf")) ||
+				((host = xmlnode_get_attrib(streamhost, "host")) &&
 				(port = xmlnode_get_attrib(streamhost, "port")) &&
-				(portnum = atoi(port))) {
-			struct bytestreams_streamhost *sh = g_new0(struct bytestreams_streamhost, 1);
+				(portnum = atoi(port))))) {
+			JabberBytestreamsStreamhost *sh = g_new0(JabberBytestreamsStreamhost, 1);
 			sh->jid = g_strdup(jid);
 			sh->host = g_strdup(host);
 			sh->port = portnum;
+			sh->zeroconf = g_strdup(zeroconf);
 			jsx->streamhosts = g_list_append(jsx->streamhosts, sh);
 		}
 	}
@@ -351,7 +407,7 @@
 			jsx->js->user->resource, xfer->who);
 
 	purple_cipher_digest_region("sha1", (guchar *)dstaddr, strlen(dstaddr),
-							  sizeof(hashval), hashval, NULL);
+				    sizeof(hashval), hashval, NULL);
 	g_free(dstaddr);
 	dstaddr = g_malloc(41);
 	p = dstaddr;
@@ -363,9 +419,12 @@
 		purple_debug_error("jabber", "someone connected with the wrong info!\n");
 		close(source);
 		purple_xfer_cancel_remote(xfer);
+		g_free(dstaddr);
 		return;
 	}
 
+	g_free(dstaddr);
+
 	g_free(jsx->rxqueue);
 	host = purple_network_get_my_ip(jsx->js->fd);
 
@@ -523,6 +582,32 @@
 		source, PURPLE_INPUT_WRITE);
 }
 
+static gint
+jabber_si_compare_jid(gconstpointer a, gconstpointer b)
+{
+	const JabberBytestreamsStreamhost *sh = a;
+
+	if(!a)
+		return -1;
+
+	return strcmp(sh->jid, (char *)b);
+}
+
+
+static void
+jabber_si_free_streamhost(gpointer data, gpointer user_data)
+{
+	JabberBytestreamsStreamhost *sh = data;
+
+	if(!data)
+		return;
+
+	g_free(sh->jid);
+	g_free(sh->host);
+	g_free(sh->zeroconf);
+	g_free(sh);
+}
+
 static void
 jabber_si_xfer_bytestreams_send_connected_cb(gpointer data, gint source,
 		PurpleInputCondition cond)
@@ -537,6 +622,7 @@
 		return;
 	else if(acceptfd == -1) {
 		purple_debug_warning("jabber", "accept: %s\n", g_strerror(errno));
+		/* TODO: This should cancel the ft */
 		return;
 	}
 
@@ -544,7 +630,61 @@
 	close(source);
 
 	xfer->watcher = purple_input_add(acceptfd, PURPLE_INPUT_READ,
-			jabber_si_xfer_bytestreams_send_read_cb, xfer);
+					 jabber_si_xfer_bytestreams_send_read_cb, xfer);
+}
+
+static void
+jabber_si_connect_proxy_cb(JabberStream *js, xmlnode *packet,
+		gpointer data)
+{
+	PurpleXfer *xfer = data;
+	JabberSIXfer *jsx = xfer->data;
+	xmlnode *query, *streamhost_used;
+	const char *from, *type, *jid;
+	GList *matched;
+
+	/* TODO: This need to send errors if we don't see what we're looking for */
+
+	/* In the case of a direct file transfer, this is expected to return */
+	if(!jsx)
+		return;
+
+	if(!(type = xmlnode_get_attrib(packet, "type")) || strcmp(type, "result"))
+		return;
+
+	if(!(from = xmlnode_get_attrib(packet, "from")))
+		return;
+
+	if(!(query = xmlnode_get_child(packet, "query")))
+		return;
+
+	if(!(streamhost_used = xmlnode_get_child(query, "streamhost-used")))
+		return;
+
+	if(!(jid = xmlnode_get_attrib(streamhost_used, "jid")))
+		return;
+
+	if(!(matched = g_list_find_custom(jsx->streamhosts, jid, jabber_si_compare_jid)))
+	{
+		gchar *my_jid = g_strdup_printf("%s@%s/%s", jsx->js->user->node,
+			jsx->js->user->domain, jsx->js->user->resource);
+		if (!strcmp(jid, my_jid))
+			purple_debug_info("jabber", "Got local SOCKS5 streamhost-used.\n");
+		else
+			purple_debug_info("jabber", "streamhost-used does not match any proxy that was offered to target\n");
+		g_free(my_jid);
+		return;
+	}
+
+	/* TODO: Clean up the local SOCKS5 proxy - it isn't going to be used.*/
+
+	jsx->streamhosts = g_list_remove_link(jsx->streamhosts, matched);
+	g_list_foreach(jsx->streamhosts, jabber_si_free_streamhost, NULL);
+	g_list_free(jsx->streamhosts);
+
+	jsx->streamhosts = matched;
+
+	jabber_si_bytestreams_attempt_connect(xfer);
 }
 
 static void
@@ -554,7 +694,10 @@
 	JabberSIXfer *jsx;
 	JabberIq *iq;
 	xmlnode *query, *streamhost;
-	char *jid, *port;
+	char *jid, port[6];
+	const char *local_ip, *public_ip, *ft_proxies;
+	GList *tmp;
+	JabberBytestreamsStreamhost *sh, *sh2;
 
 	jsx = xfer->data;
 	jsx->listen_data = NULL;
@@ -578,27 +721,106 @@
 
 	xmlnode_set_attrib(query, "sid", jsx->stream_id);
 
-	streamhost = xmlnode_new_child(query, "streamhost");
 	jid = g_strdup_printf("%s@%s/%s", jsx->js->user->node,
 			jsx->js->user->domain, jsx->js->user->resource);
-	xmlnode_set_attrib(streamhost, "jid", jid);
+	xfer->local_port = purple_network_get_port_from_fd(sock);
+	g_snprintf(port, sizeof(port), "%hu", xfer->local_port);
+
+	/* TODO: Should there be an option to not use the local host as a ft proxy?
+	 *       (to prevent revealing IP address, etc.) */
+
+	/* Include the localhost's IP (for in-network transfers) */
+	local_ip = purple_network_get_local_system_ip(jsx->js->fd);
+	if (strcmp(local_ip, "0.0.0.0") != 0)
+	{
+		streamhost = xmlnode_new_child(query, "streamhost");
+		xmlnode_set_attrib(streamhost, "jid", jid);
+		xmlnode_set_attrib(streamhost, "host", local_ip);
+		xmlnode_set_attrib(streamhost, "port", port);
+	}
+
+	/* Include the public IP (assuming that there is a port mapped somehow) */
+	/* TODO: Check that it isn't the same as above and is a valid IP */
+	public_ip = purple_network_get_my_ip(jsx->js->fd);
+	if (strcmp(public_ip, local_ip) != 0)
+	{
+		streamhost = xmlnode_new_child(query, "streamhost");
+		xmlnode_set_attrib(streamhost, "jid", jid);
+		xmlnode_set_attrib(streamhost, "host", public_ip);
+		xmlnode_set_attrib(streamhost, "port", port);
+	}
+
 	g_free(jid);
 
-	/* XXX: shouldn't we use the public IP or something? here */
-	xmlnode_set_attrib(streamhost, "host",
-			purple_network_get_my_ip(jsx->js->fd));
-	xfer->local_port = purple_network_get_port_from_fd(sock);
-	port = g_strdup_printf("%hu", xfer->local_port);
-	xmlnode_set_attrib(streamhost, "port", port);
-	g_free(port);
-
+	/* The listener for the local proxy */
 	xfer->watcher = purple_input_add(sock, PURPLE_INPUT_READ,
 			jabber_si_xfer_bytestreams_send_connected_cb, xfer);
 
-	/* XXX: insert proxies here */
+	/* insert proxies here */
+	ft_proxies = purple_account_get_string(xfer->account, "ft_proxies", NULL);
+	if (ft_proxies) {
+		int i, portnum;
+		char *tmp;
+		gchar **ft_proxy_list = g_strsplit(ft_proxies, ",", 0);
+
+		g_list_foreach(jsx->streamhosts, jabber_si_free_streamhost, NULL);
+		g_list_free(jsx->streamhosts);
+		jsx->streamhosts = NULL;
+
+		for(i = 0; ft_proxy_list[i]; i++) {
+			g_strstrip(ft_proxy_list[i]);
+			if(!(*ft_proxy_list[i]))
+				continue;
+
+			if((tmp = strchr(ft_proxy_list[i], ':'))) {
+				portnum = atoi(tmp + 1);
+				*tmp = '\0';
+			} else
+				portnum = 7777;
+
+			g_snprintf(port, sizeof(port), "%hu", portnum);
+
+			streamhost = xmlnode_new_child(query, "streamhost");
+			xmlnode_set_attrib(streamhost, "jid", ft_proxy_list[i]);
+			xmlnode_set_attrib(streamhost, "host", ft_proxy_list[i]);
+			xmlnode_set_attrib(streamhost, "port", port);
 
-	/* XXX: callback to find out which streamhost they used, or see if they
-	 * screwed it up */
+			sh = g_new0(JabberBytestreamsStreamhost, 1);
+			sh->jid = g_strdup(ft_proxy_list[i]);
+			sh->host = g_strdup(ft_proxy_list[i]);
+			sh->port = portnum;
+
+			jsx->streamhosts = g_list_prepend(jsx->streamhosts, sh);
+		}
+
+		g_strfreev(ft_proxy_list);
+	}
+
+	for (tmp = jsx->js->bs_proxies; tmp; tmp = tmp->next) {
+		sh = tmp->data;
+
+		/* TODO: deal with zeroconf proxies */
+
+		if (!(sh->host && sh->port > 0))
+			continue;
+
+		streamhost = xmlnode_new_child(query, "streamhost");
+		xmlnode_set_attrib(streamhost, "jid", sh->jid);
+		xmlnode_set_attrib(streamhost, "host", sh->host);
+		g_snprintf(port, sizeof(port), "%hu", sh->port);
+		xmlnode_set_attrib(streamhost, "port", port);
+
+		sh2 = g_new0(JabberBytestreamsStreamhost, 1);
+		sh2->jid = g_strdup(sh->jid);
+		sh2->host = g_strdup(sh->host);
+		sh2->zeroconf = g_strdup(sh->zeroconf);
+		sh2->port = sh->port;
+
+		jsx->streamhosts = g_list_prepend(jsx->streamhosts, sh2);
+	}
+
+	jabber_iq_set_callback(iq, jabber_si_connect_proxy_cb, xfer);
+
 	jabber_iq_send(iq);
 
 }
@@ -688,8 +910,7 @@
 	/* maybe later we'll do hash and date attribs */
 
 	feature = xmlnode_new_child(si, "feature");
-	xmlnode_set_namespace(feature,
-			"http://jabber.org/protocol/feature-neg");
+	xmlnode_set_namespace(feature, "http://jabber.org/protocol/feature-neg");
 	x = xmlnode_new_child(feature, "x");
 	xmlnode_set_namespace(x, "jabber:x:data");
 	xmlnode_set_attrib(x, "type", "form");
@@ -698,8 +919,7 @@
 	xmlnode_set_attrib(field, "type", "list-single");
 	option = xmlnode_new_child(field, "option");
 	value = xmlnode_new_child(option, "value");
-	xmlnode_insert_data(value, "http://jabber.org/protocol/bytestreams",
-			-1);
+	xmlnode_insert_data(value, "http://jabber.org/protocol/bytestreams", -1);
 	/*
 	option = xmlnode_new_child(field, "option");
 	value = xmlnode_new_child(option, "value");
@@ -729,6 +949,14 @@
 	if (jsx->iq_id != NULL)
 		jabber_iq_remove_callback_by_id(js, jsx->iq_id);
 
+	if (jsx->connect_timeout > 0)
+		purple_timeout_remove(jsx->connect_timeout);
+
+	if (jsx->streamhosts) {
+		g_list_foreach(jsx->streamhosts, jabber_si_free_streamhost, NULL);
+		g_list_free(jsx->streamhosts);
+	}
+
 	g_free(jsx->stream_id);
 	g_free(jsx->iq_id);
 	/* XXX: free other stuff */