/*
 * purple - Jabber Protocol Plugin
 *
 * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com>
 *
 * 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 "internal.h"

#include "blist.h"
#include "debug.h"
#include "ft.h"
#include "request.h"
#include "network.h"
#include "notify.h"

#include "buddy.h"
#include "disco.h"
#include "jabber.h"
#include "ibb.h"
#include "iq.h"
#include "si.h"

#define STREAMHOST_CONNECT_TIMEOUT 15

typedef struct _JabberSIXfer {
	JabberStream *js;

	PurpleProxyConnectData *connect_data;
	PurpleNetworkListenData *listen_data;
	guint connect_timeout;

	gboolean accepted;

	char *stream_id;
	char *iq_id;

	enum {
		STREAM_METHOD_UNKNOWN     = 0,
		STREAM_METHOD_BYTESTREAMS = 2 << 1,
		STREAM_METHOD_IBB         = 2 << 2,
		STREAM_METHOD_UNSUPPORTED = 2 << 31
	} stream_method;

	GList *streamhosts;
	PurpleProxyInfo *gpi;

	char *rxqueue;
	size_t rxlen;
	gsize rxmaxlen;
	int local_streamhost_fd;

	JabberIBBSession *ibb_session;
	guint ibb_timeout_handle;
	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)
{
	GList *xfers;

	if(!sid || !from)
		return NULL;

	for(xfers = js->file_transfers; xfers; xfers = xfers->next) {
		PurpleXfer *xfer = xfers->data;
		JabberSIXfer *jsx = xfer->data;
		if(jsx->stream_id && xfer->who &&
				!strcmp(jsx->stream_id, sid) && !strcmp(xfer->who, from))
			return xfer;
	}

	return NULL;
}

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_bytestreams_attempt_connect(PurpleXfer *xfer);

static void
jabber_si_bytestreams_connect_cb(gpointer data, gint source, const gchar *error_message)
{
	PurpleXfer *xfer = data;
	JabberSIXfer *jsx = xfer->data;
	JabberIq *iq;
	xmlnode *query, *su;
	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 ? error_message : "(null)");
		jsx->streamhosts = g_list_remove(jsx->streamhosts, streamhost);
		jabber_si_free_streamhost(streamhost, NULL);
		jabber_si_bytestreams_attempt_connect(xfer);
		return;
	}

	/* 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_ibb_timeout_remove(JabberSIXfer *jsx)
{
	if (jsx->ibb_timeout_handle) {
		purple_timeout_remove(jsx->ibb_timeout_handle);
		jsx->ibb_timeout_handle = 0;
	}
}

static gboolean
jabber_si_bytestreams_ibb_timeout_cb(gpointer data)
{
	PurpleXfer *xfer = (PurpleXfer *) data;
	JabberSIXfer *jsx = xfer->data;

	if (jsx && !jsx->ibb_session) {
		purple_debug_info("jabber",
			"jabber_si_bytestreams_ibb_timeout called and IBB session not set "
			" up yet, cancel transfer");
		jabber_si_bytestreams_ibb_timeout_remove(jsx);
		purple_xfer_cancel_local(xfer);
	}

	return FALSE;
}

static void jabber_si_bytestreams_attempt_connect(PurpleXfer *xfer)
{
	JabberSIXfer *jsx = xfer->data;
	JabberBytestreamsStreamhost *streamhost;
	JabberID *dstjid;

	if(!jsx->streamhosts) {
		JabberIq *iq = jabber_iq_new(jsx->js, JABBER_IQ_ERROR);
		xmlnode *error, *inf;

		if(jsx->iq_id)
			jabber_iq_set_id(iq, jsx->iq_id);

		xmlnode_set_attrib(iq->node, "to", xfer->who);
		error = xmlnode_new_child(iq->node, "error");
		xmlnode_set_attrib(error, "code", "404");
		xmlnode_set_attrib(error, "type", "cancel");
		inf = xmlnode_new_child(error, "item-not-found");
		xmlnode_set_namespace(inf, "urn:ietf:params:xml:ns:xmpp-stanzas");

		jabber_iq_send(iq);

		/* 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
			  did 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);
			} else {
				/* setup a timeout to cancel waiting for IBB open */
				jsx->ibb_timeout_handle = purple_timeout_add_seconds(30,
					jabber_si_bytestreams_ibb_timeout_cb, xfer);
			}
			/* if we are the receiver, just wait for IBB open, callback is
			  already set up... */
		} else {
			purple_xfer_cancel_local(xfer);
		}

		return;
	}

	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);

	/* TODO: Deal with zeroconf */

	if(dstjid != NULL && streamhost->host && streamhost->port > 0) {
		char *dstaddr, *hash;
		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);

		/* 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);

		/* Per XEP-0065, the 'host' must be SHA1(SID + from JID + to JID) */
		hash = jabber_calculate_data_sha1sum(dstaddr, strlen(dstaddr));

		jsx->connect_data = purple_proxy_connect_socks5(NULL, jsx->gpi,
				hash, 0,
				jabber_si_bytestreams_connect_cb, xfer);
		g_free(hash);
		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);
	}

	if (jsx->connect_data == NULL)
	{
		jsx->streamhosts = g_list_remove(jsx->streamhosts, streamhost);
		jabber_si_free_streamhost(streamhost, NULL);
		jabber_si_bytestreams_attempt_connect(xfer);
	}
}

void jabber_bytestreams_parse(JabberStream *js, const char *from,
                              JabberIqType type, const char *id, xmlnode *query)
{
	PurpleXfer *xfer;
	JabberSIXfer *jsx;
	xmlnode *streamhost;
	const char *sid;

	if(type != JABBER_IQ_SET)
		return;

	if(!from)
		return;

	if(!(sid = xmlnode_get_attrib(query, "sid")))
		return;

	if(!(xfer = jabber_si_xfer_find(js, sid, from)))
		return;

	jsx = xfer->data;

	if(!jsx->accepted)
		return;

	if(jsx->iq_id)
		g_free(jsx->iq_id);
	jsx->iq_id = g_strdup(id);

	for(streamhost = xmlnode_get_child(query, "streamhost"); streamhost;
			streamhost = xmlnode_get_next_twin(streamhost)) {
		const char *jid, *host = NULL, *port, *zeroconf;
		int portnum = 0;

		if((jid = xmlnode_get_attrib(streamhost, "jid")) &&
				((zeroconf = xmlnode_get_attrib(streamhost, "zeroconf")) ||
				((host = xmlnode_get_attrib(streamhost, "host")) &&
				(port = xmlnode_get_attrib(streamhost, "port")) &&
				(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);
			/* If there were a lot of these, it'd be worthwhile to prepend and reverse. */
			jsx->streamhosts = g_list_append(jsx->streamhosts, sh);
		}
	}

	jabber_si_bytestreams_attempt_connect(xfer);
}


static void
jabber_si_xfer_bytestreams_send_read_again_resp_cb(gpointer data, gint source,
		PurpleInputCondition cond)
{
	PurpleXfer *xfer = data;
	JabberSIXfer *jsx = xfer->data;
	int len;

	len = write(source, jsx->rxqueue + jsx->rxlen, jsx->rxmaxlen - jsx->rxlen);
	if (len < 0 && errno == EAGAIN)
		return;
	else if (len < 0) {
		purple_input_remove(xfer->watcher);
		xfer->watcher = 0;
		g_free(jsx->rxqueue);
		jsx->rxqueue = NULL;
		close(source);
		purple_xfer_cancel_remote(xfer);
		return;
	}
	jsx->rxlen += len;

	if (jsx->rxlen < jsx->rxmaxlen)
		return;

	purple_input_remove(xfer->watcher);
	xfer->watcher = 0;
	g_free(jsx->rxqueue);
	jsx->rxqueue = NULL;

	/* Before actually starting sending the file, we need to wait until the
	 * recipient sends the IQ result with <streamhost-used/>
	 */
	purple_debug_info("jabber", "SOCKS5 connection negotiation completed. "
					  "Waiting for IQ result to start file transfer.\n");
}

static void
jabber_si_xfer_bytestreams_send_read_again_cb(gpointer data, gint source,
		PurpleInputCondition cond)
{
	PurpleXfer *xfer = data;
	JabberSIXfer *jsx = xfer->data;
	char buffer[42]; /* 40 for DST.ADDR + 2 bytes for port number*/
	int len;
	char *dstaddr, *hash;
	const char *host;

	purple_debug_info("jabber", "in jabber_si_xfer_bytestreams_send_read_again_cb\n");

	if(jsx->rxlen < 5) {
		purple_debug_info("jabber", "reading the first 5 bytes\n");
		len = read(source, buffer, 5 - jsx->rxlen);
		if(len < 0 && errno == EAGAIN)
			return;
		else if(len <= 0) {
			purple_input_remove(xfer->watcher);
			xfer->watcher = 0;
			close(source);
			purple_xfer_cancel_remote(xfer);
			return;
		}
		jsx->rxqueue = g_realloc(jsx->rxqueue, len + jsx->rxlen);
		memcpy(jsx->rxqueue + jsx->rxlen, buffer, len);
		jsx->rxlen += len;
		return;
	} else if(jsx->rxqueue[0] != 0x05 || jsx->rxqueue[1] != 0x01 ||
			jsx->rxqueue[3] != 0x03 || jsx->rxqueue[4] != 40) {
		purple_debug_info("jabber", "Invalid socks5 conn req. header[0x%x,0x%x,0x%x,0x%x,0x%x]\n",
				  jsx->rxqueue[0], jsx->rxqueue[1], jsx->rxqueue[2],
				  jsx->rxqueue[3], jsx->rxqueue[4]);
		purple_input_remove(xfer->watcher);
		xfer->watcher = 0;
		close(source);
		purple_xfer_cancel_remote(xfer);
		return;
	} else if(jsx->rxlen - 5 <  jsx->rxqueue[4] + 2) {
		/* Upper-bound of 257 (jsx->rxlen = 5, jsx->rxqueue[4] = 0xFF) */
		unsigned short to_read = jsx->rxqueue[4] + 2 - (jsx->rxlen - 5);
		purple_debug_info("jabber", "reading %u bytes for DST.ADDR + port num (trying to read %hu now)\n",
				  jsx->rxqueue[4] + 2, to_read);
		len = read(source, buffer, to_read);
		if(len < 0 && errno == EAGAIN)
			return;
		else if(len <= 0) {
			purple_input_remove(xfer->watcher);
			xfer->watcher = 0;
			close(source);
			purple_xfer_cancel_remote(xfer);
			return;
		}
		jsx->rxqueue = g_realloc(jsx->rxqueue, len + jsx->rxlen);
		memcpy(jsx->rxqueue + jsx->rxlen, buffer, len);
		jsx->rxlen += len;
	}

	/* Have we not read all of DST.ADDR and the following 2-byte port number? */
	if(jsx->rxlen - 5 < jsx->rxqueue[4] + 2)
		return;

	purple_input_remove(xfer->watcher);
	xfer->watcher = 0;

	dstaddr = g_strdup_printf("%s%s@%s/%s%s", jsx->stream_id,
			jsx->js->user->node, jsx->js->user->domain,
			jsx->js->user->resource, xfer->who);

	/* Per XEP-0065, the 'host' must be SHA1(SID + from JID + to JID) */
	hash = jabber_calculate_data_sha1sum(dstaddr, strlen(dstaddr));

	if(strncmp(hash, jsx->rxqueue + 5, 40) ||
			jsx->rxqueue[45] != 0x00 || jsx->rxqueue[46] != 0x00) {
		if (jsx->rxqueue[45] != 0x00 || jsx->rxqueue[46] != 0x00)
			purple_debug_error("jabber", "Got SOCKS5 BS conn with the wrong DST.PORT"
						     " (must be 0 - got[0x%x,0x%x]).\n",
						     jsx->rxqueue[45], jsx->rxqueue[46]);
		else
			purple_debug_error("jabber", "Got SOCKS5 BS conn with the wrong DST.ADDR"
						     " (expected '%s' - got '%.40s').\n",
						     hash, jsx->rxqueue + 5);
		close(source);
		purple_xfer_cancel_remote(xfer);
		g_free(hash);
		g_free(dstaddr);
		return;
	}

	g_free(hash);
	g_free(dstaddr);

	g_free(jsx->rxqueue);
	host = purple_network_get_my_ip(jsx->js->fd);

	jsx->rxmaxlen = 5 + strlen(host) + 2;
	jsx->rxqueue = g_malloc(jsx->rxmaxlen);
	jsx->rxlen = 0;

	jsx->rxqueue[0] = 0x05;
	jsx->rxqueue[1] = 0x00;
	jsx->rxqueue[2] = 0x00;
	jsx->rxqueue[3] = 0x03;
	jsx->rxqueue[4] = strlen(host);
	memcpy(jsx->rxqueue + 5, host, strlen(host));
	jsx->rxqueue[5+strlen(host)] = 0x00;
	jsx->rxqueue[6+strlen(host)] = 0x00;

	xfer->watcher = purple_input_add(source, PURPLE_INPUT_WRITE,
		jabber_si_xfer_bytestreams_send_read_again_resp_cb, xfer);
	jabber_si_xfer_bytestreams_send_read_again_resp_cb(xfer, source,
		PURPLE_INPUT_WRITE);
}

static void
jabber_si_xfer_bytestreams_send_read_response_cb(gpointer data, gint source,
		PurpleInputCondition cond)
{
	PurpleXfer *xfer = data;
	JabberSIXfer *jsx = xfer->data;
	int len;

	len = write(source, jsx->rxqueue + jsx->rxlen, jsx->rxmaxlen - jsx->rxlen);
	if (len < 0 && errno == EAGAIN)
		return;
	else if (len < 0) {
		purple_input_remove(xfer->watcher);
		xfer->watcher = 0;
		g_free(jsx->rxqueue);
		jsx->rxqueue = NULL;
		close(source);
		purple_xfer_cancel_remote(xfer);
		return;
	}
	jsx->rxlen += len;

	if (jsx->rxlen < jsx->rxmaxlen)
		return;

	purple_input_remove(xfer->watcher);
	xfer->watcher = 0;

	/* If we sent a "Success", wait for a response, otherwise give up and cancel */
	if (jsx->rxqueue[1] == 0x00) {
		xfer->watcher = purple_input_add(source, PURPLE_INPUT_READ,
			jabber_si_xfer_bytestreams_send_read_again_cb, xfer);
		g_free(jsx->rxqueue);
		jsx->rxqueue = NULL;
		jsx->rxlen = 0;
	} else {
		close(source);
		purple_xfer_cancel_remote(xfer);
	}
}

static void
jabber_si_xfer_bytestreams_send_read_cb(gpointer data, gint source,
		PurpleInputCondition cond)
{
	PurpleXfer *xfer = data;
	JabberSIXfer *jsx = xfer->data;
	int i;
	int len;
	char buffer[256];

	purple_debug_info("jabber", "in jabber_si_xfer_bytestreams_send_read_cb\n");

	xfer->fd = source;

	/** Try to read the SOCKS5 header */
	if(jsx->rxlen < 2) {
		purple_debug_info("jabber", "reading those first two bytes\n");
		len = read(source, buffer, 2 - jsx->rxlen);
		if(len < 0 && errno == EAGAIN)
			return;
		else if(len <= 0) {
			purple_input_remove(xfer->watcher);
			xfer->watcher = 0;
			close(source);
			purple_xfer_cancel_remote(xfer);
			return;
		}
		jsx->rxqueue = g_realloc(jsx->rxqueue, len + jsx->rxlen);
		memcpy(jsx->rxqueue + jsx->rxlen, buffer, len);
		jsx->rxlen += len;
		return;
	} else if(jsx->rxlen - 2 < jsx->rxqueue[1]) {
		/* Has a maximum value of 255 (jsx->rxlen = 2, jsx->rxqueue[1] = 0xFF) */
		unsigned short to_read = jsx->rxqueue[1] - (jsx->rxlen - 2);
		purple_debug_info("jabber", "reading %u bytes for auth methods (trying to read %hu now)\n",
				  jsx->rxqueue[1], to_read);
		len = read(source, buffer, to_read);
		if(len < 0 && errno == EAGAIN)
			return;
		else if(len <= 0) {
			purple_input_remove(xfer->watcher);
			xfer->watcher = 0;
			close(source);
			purple_xfer_cancel_remote(xfer);
			return;
		}
		jsx->rxqueue = g_realloc(jsx->rxqueue, len + jsx->rxlen);
		memcpy(jsx->rxqueue + jsx->rxlen, buffer, len);
		jsx->rxlen += len;
	}

	/* Have we not read all the auth. method bytes? */
	if(jsx->rxlen -2 < jsx->rxqueue[1])
		return;

	purple_input_remove(xfer->watcher);
	xfer->watcher = 0;

	purple_debug_info("jabber", "checking to make sure we're socks FIVE\n");

	if(jsx->rxqueue[0] != 0x05) {
		close(source);
		purple_xfer_cancel_remote(xfer);
		return;
	}

	purple_debug_info("jabber", "going to test %hhu different methods\n", jsx->rxqueue[1]);

	for(i=0; i<jsx->rxqueue[1]; i++) {

		purple_debug_info("jabber", "testing %hhu\n", jsx->rxqueue[i+2]);
		if(jsx->rxqueue[i+2] == 0x00) {
			g_free(jsx->rxqueue);
			jsx->rxlen = 0;
			jsx->rxmaxlen = 2;
			jsx->rxqueue = g_malloc(jsx->rxmaxlen);
			jsx->rxqueue[0] = 0x05;
			jsx->rxqueue[1] = 0x00;
			xfer->watcher = purple_input_add(source, PURPLE_INPUT_WRITE,
				jabber_si_xfer_bytestreams_send_read_response_cb,
				xfer);
			jabber_si_xfer_bytestreams_send_read_response_cb(xfer,
				source, PURPLE_INPUT_WRITE);
			jsx->rxqueue = NULL;
			jsx->rxlen = 0;
			return;
		}
	}

	g_free(jsx->rxqueue);
	jsx->rxlen = 0;
	jsx->rxmaxlen = 2;
	jsx->rxqueue = g_malloc(jsx->rxmaxlen);
	jsx->rxqueue[0] = 0x05;
	jsx->rxqueue[1] = 0xFF;
	xfer->watcher = purple_input_add(source, PURPLE_INPUT_WRITE,
		jabber_si_xfer_bytestreams_send_read_response_cb, xfer);
	jabber_si_xfer_bytestreams_send_read_response_cb(xfer,
		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_xfer_bytestreams_send_connected_cb(gpointer data, gint source,
		PurpleInputCondition cond)
{
	PurpleXfer *xfer = data;
	JabberSIXfer *jsx = xfer->data;
	int acceptfd, flags;

	purple_debug_info("jabber", "in jabber_si_xfer_bytestreams_send_connected_cb\n");

	acceptfd = accept(source, NULL, 0);
	if(acceptfd == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
		return;
	else if(acceptfd == -1) {
		purple_debug_warning("jabber", "accept: %s\n", g_strerror(errno));
		/* Don't cancel the ft - allow it to fall to the next streamhost.*/
		return;
	}

	purple_input_remove(xfer->watcher);
	close(source);
	jsx->local_streamhost_fd = -1;

	flags = fcntl(acceptfd, F_GETFL);
	fcntl(acceptfd, F_SETFL, flags | O_NONBLOCK);
#ifndef _WIN32
	fcntl(acceptfd, F_SETFD, FD_CLOEXEC);
#endif

	xfer->watcher = purple_input_add(acceptfd, PURPLE_INPUT_READ,
					 jabber_si_xfer_bytestreams_send_read_cb, xfer);
}

static void
jabber_si_connect_proxy_cb(JabberStream *js, const char *from,
                           JabberIqType type, const char *id,
                           xmlnode *packet, gpointer data)
{
	PurpleXfer *xfer = data;
	JabberSIXfer *jsx;
	xmlnode *query, *streamhost_used;
	const char *jid;
	GList *matched;

	/* TODO: This need to send errors if we don't see what we're looking for */

	/* Make sure that the xfer is actually still valid and we're not just receiving an old iq response */
	if (!g_list_find(js->file_transfers, xfer)) {
		purple_debug_error("jabber", "Got bytestreams response for no longer existing xfer (%p)\n", xfer);
		return;
	}

	/* In the case of a direct file transfer, this is expected to return */
	if(!xfer->data)
		return;

	jsx = xfer->data;

	if(type != JABBER_IQ_RESULT) {
		purple_debug_info("jabber",
			    "jabber_si_xfer_connect_proxy_cb: type = error\n");
		/* 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);
			} else {
				jsx->ibb_timeout_handle = purple_timeout_add_seconds(30,
					jabber_si_bytestreams_ibb_timeout_cb, xfer);
			}
			/* if we are receiver, just wait for IBB open stanza, callback
			  is already set up */
		} else {
			purple_xfer_cancel_remote(xfer);
		}
		return;
	}

	if (!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;

	purple_debug_info("jabber", "jabber_si_connect_proxy_cb() will be looking at jsx %p: jsx->streamhosts is %p and jid is %s\n",
					  jsx, jsx->streamhosts, jid);

	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");
			purple_xfer_start(xfer, xfer->fd, NULL, -1);
		} else {
			/* 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);
				} else {
					jsx->ibb_timeout_handle = purple_timeout_add_seconds(30,
						jabber_si_bytestreams_ibb_timeout_cb, 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;
	}

	/* Clean up the local streamhost - it isn't going to be used.*/
	if (xfer->watcher > 0) {
		purple_input_remove(xfer->watcher);
		xfer->watcher = 0;
	}
	if (jsx->local_streamhost_fd >= 0) {
		close(jsx->local_streamhost_fd);
		jsx->local_streamhost_fd = -1;
	}

	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
jabber_si_xfer_bytestreams_listen_cb(int sock, gpointer data)
{
	PurpleXfer *xfer = data;
	JabberSIXfer *jsx;
	JabberIq *iq;
	xmlnode *query, *streamhost;
	char port[6];
	GList *tmp;
	JabberBytestreamsStreamhost *sh, *sh2;
	int streamhost_count = 0;

	jsx = xfer->data;
	jsx->listen_data = NULL;

	/* I'm not sure under which conditions this can happen
	 * (it seems like it shouldn't be possible */
	if (purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_CANCEL_LOCAL) {
		purple_xfer_unref(xfer);
		return;
	}

	purple_xfer_unref(xfer);

	iq = jabber_iq_new_query(jsx->js, JABBER_IQ_SET,
			"http://jabber.org/protocol/bytestreams");
	xmlnode_set_attrib(iq->node, "to", xfer->who);
	query = xmlnode_get_child(iq->node, "query");

	xmlnode_set_attrib(query, "sid", jsx->stream_id);

	/* If we successfully started listening locally */
	if (sock >= 0) {
		gchar *jid;
		const char *local_ip, *public_ip;

		jsx->local_streamhost_fd = sock;

		jid = g_strdup_printf("%s@%s/%s", jsx->js->user->node,
			jsx->js->user->domain, jsx->js->user->resource);
		xfer->local_port = purple_network_get_port_from_fd(sock);
		g_snprintf(port, sizeof(port), "%hu", xfer->local_port);

		/* 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_count++;
			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) */
		public_ip = purple_network_get_my_ip(jsx->js->fd);
		if (strcmp(public_ip, local_ip) != 0 && strcmp(public_ip, "0.0.0.0") != 0) {
			streamhost_count++;
			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);

		/* The listener for the local proxy */
		xfer->watcher = purple_input_add(sock, PURPLE_INPUT_READ,
				jabber_si_xfer_bytestreams_send_connected_cb, xfer);
	}

	for (tmp = jsx->js->bs_proxies; tmp; tmp = tmp->next) {
		sh = tmp->data;

		/* TODO: deal with zeroconf proxies */

		if (!(sh->jid && sh->host && sh->port > 0))
			continue;

		purple_debug_info("jabber", "jabber_si_xfer_bytestreams_listen_cb() will be looking at jsx %p: jsx->streamhosts %p and sh->jid %p\n",
						  jsx, jsx->streamhosts, sh->jid);
		if(g_list_find_custom(jsx->streamhosts, sh->jid, jabber_si_compare_jid) != NULL)
			continue;

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

	/* We have no way of transferring, cancel the transfer */
	if (streamhost_count == 0) {
		jabber_iq_free(iq);

		/* 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);
			} else {
				jsx->ibb_timeout_handle = purple_timeout_add_seconds(30,
					jabber_si_bytestreams_ibb_timeout_cb, 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;
	}

	jabber_iq_set_callback(iq, jabber_si_connect_proxy_cb, xfer);

	jabber_iq_send(iq);

}

static void
jabber_si_xfer_bytestreams_send_init(PurpleXfer *xfer)
{
	JabberSIXfer *jsx;

	purple_xfer_ref(xfer);

	jsx = xfer->data;

	/* TODO: Should there be an option to not use the local host as a ft proxy?
	 *       (to prevent revealing IP address, etc.) */
	jsx->listen_data = purple_network_listen_range(0, 0, SOCK_STREAM,
				jabber_si_xfer_bytestreams_listen_cb, xfer);
	if (jsx->listen_data == NULL) {
		/* We couldn't open a local port.  Perhaps we can use a proxy. */
		jabber_si_xfer_bytestreams_listen_cb(-1, 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 occurred during IBB file transfer\n");
	purple_xfer_error(purple_xfer_get_type(xfer), account,
		jabber_ibb_session_get_who(sess),
			_("An error occurred on the in-band bytestream transfer\n"));
	purple_xfer_cancel_remote(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_cancel_remote(xfer);
	} else {
		purple_xfer_set_completed(xfer, TRUE);
		purple_xfer_end(xfer);
	}
}

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)) {
		purple_debug_info("jabber", "about to write %" G_GSIZE_FORMAT " bytes from IBB stream\n",
			size);
		if(!fwrite(data, size, 1, jsx->fp)) {
			purple_debug_error("jabber", "error writing to file\n");
			purple_xfer_cancel_remote(xfer);
			return;
		}
		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);
			purple_xfer_end(xfer);
		}
	} 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 send more data than expected\n");
		purple_xfer_cancel_remote(xfer);
	}

}

static gboolean
jabber_si_xfer_ibb_open_cb(JabberStream *js, const char *who, const char *id,
                           xmlnode *open)
{
	const gchar *sid = xmlnode_get_attrib(open, "sid");
	PurpleXfer *xfer = jabber_si_xfer_find(js, sid, who);
	if (xfer) {
		JabberSIXfer *jsx = (JabberSIXfer *) xfer->data;
		JabberIBBSession *sess =
			jabber_ibb_session_create_from_xmlnode(js, who, id, open, xfer);
		const char *filename;

		jabber_si_bytestreams_ibb_timeout_remove(jsx);

		if (sess) {
			/* open the file to write to */
			filename = purple_xfer_get_local_filename(xfer);
			jsx->fp = g_fopen(filename, "wb");
			if (jsx->fp == NULL) {
				purple_debug_error("jabber", "failed to open file %s for writing: %s\n",
					filename, g_strerror(errno));
				purple_xfer_cancel_remote(xfer);
				return FALSE;
			}

			/* 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);

			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");
			purple_xfer_cancel_remote(xfer);
			return FALSE;
		}
	} else {
		/* we got an IBB <open/> for an unknown file transfer, pass along... */
		purple_debug_info("jabber",
			"IBB open did not match any SI file transfer\n");
		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 %" G_GSIZE_FORMAT " bytes from file %p\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_send_data: error reading from file\n");
		purple_xfer_cancel_local(xfer);
	}
	g_free(data);
}

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);
		purple_xfer_end(xfer);
	} 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;
	JabberStream *js = jabber_ibb_session_get_js(sess);
	PurpleConnection *gc = js->gc;
	PurpleAccount *account = purple_connection_get_account(gc);

	if (jabber_ibb_session_get_state(sess) == JABBER_IBB_SESSION_OPENED) {
		const char *filename = purple_xfer_get_local_filename(xfer);
		jsx->fp = g_fopen(filename, "rb");
		if (jsx->fp == NULL) {
			purple_debug_error("jabber", "Failed to open file %s for reading: %s\n",
				filename, g_strerror(errno));
			purple_xfer_error(purple_xfer_get_type(xfer), account,
				jabber_ibb_session_get_who(sess),
				_("Failed to open the file"));
			purple_xfer_cancel_local(xfer);
			return;
		}

		purple_xfer_start(xfer, 0, NULL, 0);
		purple_xfer_set_bytes_sent(xfer, 0);
		purple_xfer_update_progress(xfer);
		jabber_si_xfer_ibb_send_data(sess);
	} else {
		/* error */
		purple_xfer_error(purple_xfer_get_type(xfer), account,
			jabber_ibb_session_get_who(sess),
			_("Failed to open in-band bytestream"));
		purple_xfer_end(xfer);
	}
}

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_debug_error("jabber",
			"failed to initiate IBB session for file transfer\n");
		purple_xfer_cancel_local(xfer);
	}
}

static void jabber_si_xfer_send_method_cb(JabberStream *js, const char *from,
                                          JabberIqType type, const char *id,
                                          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);
		return;
	}

	if(!(feature = xmlnode_get_child_with_namespace(si, "feature", "http://jabber.org/protocol/feature-neg"))) {
		purple_xfer_cancel_remote(xfer);
		return;
	}

	if(!(x = xmlnode_get_child_with_namespace(feature, "x", "jabber:x:data"))) {
		purple_xfer_cancel_remote(xfer);
		return;
	}

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

	if (!found_method) {
		purple_xfer_cancel_remote(xfer);
	}

}

static void jabber_si_xfer_send_request(PurpleXfer *xfer)
{
	JabberSIXfer *jsx = xfer->data;
	JabberIq *iq;
	xmlnode *si, *file, *feature, *x, *field, *option, *value;
	char buf[32];

	xfer->filename = g_path_get_basename(xfer->local_filename);

	iq = jabber_iq_new(jsx->js, JABBER_IQ_SET);
	xmlnode_set_attrib(iq->node, "to", xfer->who);
	si = xmlnode_new_child(iq->node, "si");
	xmlnode_set_namespace(si, "http://jabber.org/protocol/si");
	jsx->stream_id = jabber_get_next_id(jsx->js);
	xmlnode_set_attrib(si, "id", jsx->stream_id);
	xmlnode_set_attrib(si, "profile",
			"http://jabber.org/protocol/si/profile/file-transfer");

	file = xmlnode_new_child(si, "file");
	xmlnode_set_namespace(file,
			"http://jabber.org/protocol/si/profile/file-transfer");
	xmlnode_set_attrib(file, "name", xfer->filename);
	g_snprintf(buf, sizeof(buf), "%" G_GSIZE_FORMAT, xfer->size);
	xmlnode_set_attrib(file, "size", buf);
	/* 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");
	x = xmlnode_new_child(feature, "x");
	xmlnode_set_namespace(x, "jabber:x:data");
	xmlnode_set_attrib(x, "type", "form");
	field = xmlnode_new_child(x, "field");
	xmlnode_set_attrib(field, "var", "stream-method");
	xmlnode_set_attrib(field, "type", "list-single");
	/* maybe we should add an option to always skip bytestreams for people
		behind troublesome firewalls */
	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);

	/* Store the IQ id so that we can cancel the callback */
	g_free(jsx->iq_id);
	jsx->iq_id = g_strdup(iq->id);

	jabber_iq_send(iq);
}

static void jabber_si_xfer_free(PurpleXfer *xfer)
{
	JabberSIXfer *jsx = xfer->data;

	if (jsx) {
		JabberStream *js = jsx->js;

		js->file_transfers = g_list_remove(js->file_transfers, xfer);

		if (jsx->connect_data != NULL)
			purple_proxy_connect_cancel(jsx->connect_data);
		if (jsx->listen_data != NULL)
			purple_network_listen_cancel(jsx->listen_data);
		if (jsx->iq_id != NULL)
			jabber_iq_remove_callback_by_id(js, jsx->iq_id);
		if (jsx->local_streamhost_fd >= 0)
			close(jsx->local_streamhost_fd);
		if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND &&
			xfer->fd >= 0) {
			purple_debug_info("jabber", "remove port mapping\n");
			purple_network_remove_port_mapping(xfer->fd);
		}
		if (jsx->connect_timeout > 0)
			purple_timeout_remove(jsx->connect_timeout);
		if (jsx->ibb_timeout_handle > 0)
			purple_timeout_remove(jsx->ibb_timeout_handle);

		if (jsx->streamhosts) {
			g_list_foreach(jsx->streamhosts, jabber_si_free_streamhost, NULL);
			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) {
			purple_debug_info("jabber",
				"jabber_si_xfer_free: closing file for IBB transfer\n");
			fclose(jsx->fp);
		}

		g_free(jsx->stream_id);
		g_free(jsx->iq_id);
		/* XXX: free other stuff */
		g_free(jsx->rxqueue);
		g_free(jsx);
		xfer->data = NULL;

		purple_debug_info("jabber", "jabber_si_xfer_free(): freeing jsx %p\n", jsx);
	}
}

/*
 * These four functions should only be called from the PurpleXfer functions
 * (typically purple_xfer_cancel_(remote|local), purple_xfer_end, or
 * purple_xfer_request_denied.
 */
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");
}


static void jabber_si_xfer_request_denied(PurpleXfer *xfer)
{
	JabberSIXfer *jsx = (JabberSIXfer *) xfer->data;
	JabberStream *js = jsx->js;

	/*
	 * TODO: It's probably an error if jsx->iq_id == NULL. g_return_if_fail
	 * might be warranted.
	 */
	if (jsx->iq_id && !jsx->accepted) {
		JabberIq *iq;
		xmlnode *error, *child;
		iq = jabber_iq_new(js, JABBER_IQ_ERROR);
		xmlnode_set_attrib(iq->node, "to", xfer->who);
		jabber_iq_set_id(iq, jsx->iq_id);

		error = xmlnode_new_child(iq->node, "error");
		xmlnode_set_attrib(error, "type", "cancel");
		child = xmlnode_new_child(error, "forbidden");
		xmlnode_set_namespace(child, "urn:ietf:params:xml:ns:xmpp-stanzas");
		child = xmlnode_new_child(error, "text");
		xmlnode_set_namespace(child, "urn:ietf:params:xml:ns:xmpp-stanzas");
		xmlnode_insert_data(child, "Offer Declined", -1);

		jabber_iq_send(iq);
	}

	jabber_si_xfer_free(xfer);
	purple_debug(PURPLE_DEBUG_INFO, "jabber", "in jabber_si_xfer_request_denied\n");
}


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");
}


static void jabber_si_xfer_end(PurpleXfer *xfer)
{
	jabber_si_xfer_free(xfer);
}


static void jabber_si_xfer_send_disco_cb(JabberStream *js, const char *who,
		JabberCapabilities capabilities, gpointer 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) {
		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);
		purple_notify_error(js->gc, _("File Send Failed"),
				_("File Send Failed"), msg);
		g_free(msg);
		purple_xfer_cancel_local(xfer);
	}
}

static void resource_select_cancel_cb(PurpleXfer *xfer, PurpleRequestFields *fields)
{
	purple_xfer_cancel_local(xfer);
}

static void do_transfer_send(PurpleXfer *xfer, const char *resource)
{
	JabberSIXfer *jsx = xfer->data;
	char **who_v = g_strsplit(xfer->who, "/", 2);
	char *who;
	JabberBuddy *jb;
	JabberBuddyResource *jbr = NULL;

	jb = jabber_buddy_find(jsx->js, who_v[0], FALSE);
	if (jb) {
		jbr = jabber_buddy_find_resource(jb, resource);
	}

	who = g_strdup_printf("%s/%s", who_v[0], resource);
	g_strfreev(who_v);
	g_free(xfer->who);
	xfer->who = who;

	if (jbr) {
		char *msg;

		if (jabber_resource_has_capability(jbr, XEP_0047_NAMESPACE))
			jsx->stream_method |= STREAM_METHOD_IBB;
		if (jabber_resource_has_capability(jbr, "http://jabber.org/protocol/si/profile/file-transfer")) {
			jabber_si_xfer_send_request(xfer);
			return;
		}

		msg = g_strdup_printf(_("Unable to send file to %s, user does not support file transfers"), who);
		purple_notify_error(jsx->js->gc, _("File Send Failed"),
				_("File Send Failed"), msg);
		g_free(msg);
		purple_xfer_cancel_local(xfer);
	} else {
		jabber_disco_info_do(jsx->js, who,
				jabber_si_xfer_send_disco_cb, xfer);
	}
}

static void resource_select_ok_cb(PurpleXfer *xfer, PurpleRequestFields *fields)
{
	PurpleRequestField *field = purple_request_fields_get_field(fields, "resource");
	int selected_id = purple_request_field_choice_get_value(field);
	GList *labels = purple_request_field_choice_get_labels(field);

	const char *selected_label = g_list_nth_data(labels, selected_id);

	do_transfer_send(xfer, selected_label);
}

static void jabber_si_xfer_init(PurpleXfer *xfer)
{
	JabberSIXfer *jsx = xfer->data;
	JabberIq *iq;
	if(purple_xfer_get_type(xfer) == PURPLE_XFER_SEND) {
		JabberBuddy *jb;
		JabberBuddyResource *jbr = NULL;
		char *resource;

		if(NULL != (resource = jabber_get_resource(xfer->who))) {
			/* they've specified a resource, no need to ask or
			 * default or anything, just do it */

			do_transfer_send(xfer, resource);
			g_free(resource);
			return;
		}

		jb = jabber_buddy_find(jsx->js, xfer->who, TRUE);

		if(!jb || !jb->resources) {
			/* no resources online, we're trying to send to someone
			 * whose presence we're not subscribed to, or
			 * someone who is offline.  Let's inform the user */
			char *msg;

			if(!jb) {
				msg = g_strdup_printf(_("Unable to send file to %s, invalid JID"), xfer->who);
			} else if(jb->subscription & JABBER_SUB_TO) {
				msg = g_strdup_printf(_("Unable to send file to %s, user is not online"), xfer->who);
			} else {
				msg = g_strdup_printf(_("Unable to send file to %s, not subscribed to user presence"), xfer->who);
			}

			purple_notify_error(jsx->js->gc, _("File Send Failed"), _("File Send Failed"), msg);
			g_free(msg);
		} else if(!jb->resources->next) {
			/* only 1 resource online (probably our most common case)
			 * so no need to ask who to send to */
			jbr = jb->resources->data;

			do_transfer_send(xfer, jbr->name);

		} else {
			/* we've got multiple resources, we need to pick one to send to */
			GList *l;
			char *msg = g_strdup_printf(_("Please select the resource of %s to which you would like to send a file"), xfer->who);
			PurpleRequestFields *fields = purple_request_fields_new();
			PurpleRequestField *field = purple_request_field_choice_new("resource", _("Resource"), 0);
			PurpleRequestFieldGroup *group = purple_request_field_group_new(NULL);

			for(l = jb->resources; l; l = l->next)
			{
				jbr = l->data;

				purple_request_field_choice_add(field, jbr->name);
			}

			purple_request_field_group_add_field(group, field);

			purple_request_fields_add_group(fields, group);

			purple_request_fields(jsx->js->gc, _("Select a Resource"), msg, NULL, fields,
					_("Send File"), G_CALLBACK(resource_select_ok_cb), _("Cancel"), G_CALLBACK(resource_select_cancel_cb),
					jsx->js->gc->account, xfer->who, NULL, xfer);

			g_free(msg);
		}
	} else {
		xmlnode *si, *feature, *x, *field, *value;

		iq = jabber_iq_new(jsx->js, JABBER_IQ_RESULT);
		xmlnode_set_attrib(iq->node, "to", xfer->who);
		if(jsx->iq_id)
			jabber_iq_set_id(iq, jsx->iq_id);
		else
			purple_debug_error("jabber", "Sending SI result with new IQ id.\n");

		jsx->accepted = TRUE;

		si = xmlnode_new_child(iq->node, "si");
		xmlnode_set_namespace(si, "http://jabber.org/protocol/si");

		feature = xmlnode_new_child(si, "feature");
		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", "submit");
		field = xmlnode_new_child(x, "field");
		xmlnode_set_attrib(field, "var", "stream-method");

		/* 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 */
		/* maybe we should have an account option to always just try IBB
			for people who know their firewalls are very restrictive */
		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) {
			value = xmlnode_new_child(field, "value");
			xmlnode_insert_data(value, "http://jabber.org/protocol/ibb", -1);
		}

		jabber_iq_send(iq);
	}
}

PurpleXfer *jabber_si_new_xfer(PurpleConnection *gc, const char *who)
{
	JabberStream *js;

	PurpleXfer *xfer;
	JabberSIXfer *jsx;

	js = gc->proto_data;

	xfer = purple_xfer_new(gc->account, PURPLE_XFER_SEND, who);
	if (xfer)
	{
		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);
		purple_xfer_set_end_fnc(xfer, jabber_si_xfer_end);

		js->file_transfers = g_list_append(js->file_transfers, xfer);
	}

	return xfer;
}

void jabber_si_xfer_send(PurpleConnection *gc, const char *who, const char *file)
{
	JabberStream *js;

	PurpleXfer *xfer;

	js = gc->proto_data;

	xfer = jabber_si_new_xfer(gc, who);

	if (file)
		purple_xfer_request_accepted(xfer, file);
	else
		purple_xfer_request(xfer);
}

void jabber_si_parse(JabberStream *js, const char *from, JabberIqType type,
                     const char *id, xmlnode *si)
{
	JabberSIXfer *jsx;
	PurpleXfer *xfer;
	xmlnode *file, *feature, *x, *field, *option, *value;
	const char *stream_id, *filename, *filesize_c, *profile;
	size_t filesize = 0;

	if(!(profile = xmlnode_get_attrib(si, "profile")) ||
			strcmp(profile, "http://jabber.org/protocol/si/profile/file-transfer"))
		return;

	if(!(stream_id = xmlnode_get_attrib(si, "id")))
		return;

	if(!(file = xmlnode_get_child(si, "file")))
		return;

	if(!(filename = xmlnode_get_attrib(file, "name")))
		return;

	if((filesize_c = xmlnode_get_attrib(file, "size")))
		filesize = atoi(filesize_c);

	if(!(feature = xmlnode_get_child(si, "feature")))
		return;

	if(!(x = xmlnode_get_child_with_namespace(feature, "x", "jabber:x:data")))
		return;

	if(!from)
		return;

	/* if they've already sent us this file transfer with the same damn id
	 * then we're gonna ignore it, until I think of something better to do
	 * with it */
	if((xfer = jabber_si_xfer_find(js, stream_id, from)))
		return;

	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");
		if(var && !strcmp(var, "stream-method")) {
			for(option = xmlnode_get_child(field, "option"); option;
					option = xmlnode_get_next_twin(option)) {
				if((value = xmlnode_get_child(option, "value"))) {
					char *val;
					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);
					}
				}
			}
		}
	}

	if(jsx->stream_method == STREAM_METHOD_UNKNOWN) {
		g_free(jsx);
		return;
	}

	jsx->js = js;
	jsx->stream_id = g_strdup(stream_id);
	jsx->iq_id = g_strdup(id);

	xfer = purple_xfer_new(js->gc->account, PURPLE_XFER_RECEIVE, from);
	g_return_if_fail(xfer != NULL);

	xfer->data = jsx;

	purple_xfer_set_filename(xfer, filename);
	if(filesize > 0)
		purple_xfer_set_size(xfer, filesize);

	purple_xfer_set_init_fnc(xfer, jabber_si_xfer_init);
	purple_xfer_set_request_denied_fnc(xfer, jabber_si_xfer_request_denied);
	purple_xfer_set_cancel_recv_fnc(xfer, jabber_si_xfer_cancel_recv);
	purple_xfer_set_end_fnc(xfer, jabber_si_xfer_end);

	js->file_transfers = g_list_append(js->file_transfers, xfer);

	purple_xfer_request(xfer);
}

void
jabber_si_init(void)
{
	jabber_iq_register_handler("si", "http://jabber.org/protocol/si", jabber_si_parse);

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

