libpurple/protocols/jabber/si.c

Tue, 20 Feb 2024 00:55:28 -0600

author
Gary Kramlich <grim@reaperworld.com>
date
Tue, 20 Feb 2024 00:55:28 -0600
changeset 42592
6b65c0e4ba15
parent 42570
a980db2607fd
permissions
-rw-r--r--

Remove unnecessary casts for GObject methods

Testing Done:
Compiled with the turtles and verified no new warnings appeared.

Reviewed at https://reviews.imfreedom.org/r/2956/

/*
 * purple - Jabber Protocol Plugin
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * 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 <purpleconfig.h>

#include <errno.h>
#include <sys/types.h>

#include <glib/gi18n-lib.h>

#include <purple.h>

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

#define STREAMHOST_CONNECT_TIMEOUT 5
#define ENABLE_FT_THUMBNAILS 0

struct _JabberSIXfer {
	PurpleXfer parent;

	JabberStream *js;

	GCancellable *cancellable;
	GSocketClient *client;
	GSocketService *service;
	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 << 30
	} stream_method;

	GList *streamhosts;

	gchar *socks_buf;
	GSocketConnection *local_streamhost_conn;

	JabberIBBSession *ibb_session;
	guint ibb_timeout_handle;
	PurpleCircularBuffer *ibb_buffer;
};

G_DEFINE_DYNAMIC_TYPE_EXTENDED(JabberSIXfer, jabber_si_xfer, PURPLE_TYPE_XFER,
                               G_TYPE_FLAG_FINAL, {})

/* 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 = JABBER_SI_XFER(xfer);
		if(jsx->stream_id && purple_xfer_get_remote_user(xfer) &&
				purple_strequal(jsx->stream_id, sid) && purple_strequal(purple_xfer_get_remote_user(xfer), from))
			return xfer;
	}

	return NULL;
}



static void jabber_si_bytestreams_attempt_connect(PurpleXfer *xfer);

static void
jabber_si_bytestreams_try_next_streamhost(PurpleXfer *xfer,
                                          const gchar *error_message)
{
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	JabberBytestreamsStreamhost *streamhost = jsx->streamhosts->data;

	purple_debug_warning(
	        "jabber",
	        "si connection failed, jid was %s, host was %s, error was %s",
	        streamhost->jid, streamhost->host, error_message);
	jsx->streamhosts = g_list_remove(jsx->streamhosts, streamhost);
	jabber_bytestreams_streamhost_free(streamhost);
	g_clear_object(&jsx->client);
	jabber_si_bytestreams_attempt_connect(xfer);
}

static void
jabber_si_bytestreams_connect_cb(GObject *source, GAsyncResult *result,
                                 gpointer user_data)
{
	PurpleXfer *xfer = PURPLE_XFER(user_data);
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	GIOStream *stream = NULL;
	GError *error = NULL;
	GSocket *socket = NULL;
	JabberIq *iq = NULL;
	JabberBytestreamsStreamhost *streamhost = jsx->streamhosts->data;

	stream = g_proxy_connect_finish(G_PROXY(source), result, &error);
	if (stream == NULL) {
		if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
			purple_debug_error("jabber",
			                   "Unable to connect to destination host: %s",
			                   error->message);
			jabber_si_bytestreams_try_next_streamhost(
			        xfer, "Unable to connect to destination host.");
		}

		g_clear_error(&error);
		return;
	}

	if (!G_IS_SOCKET_CONNECTION(stream)) {
		purple_debug_error("jabber",
		                   "GProxy didn't return a GSocketConnection.");
		jabber_si_bytestreams_try_next_streamhost(
		        xfer, "GProxy didn't return a GSocketConnection.");
		g_object_unref(stream);
		return;
	}

	/* unknown file transfer type is assumed to be RECEIVE */
	if (purple_xfer_get_xfer_type(xfer) == PURPLE_XFER_TYPE_SEND) {
		PurpleXmlNode *query, *activate;
		iq = jabber_iq_new_query(jsx->js, JABBER_IQ_SET, NS_BYTESTREAMS);
		purple_xmlnode_set_attrib(iq->node, "to", streamhost->jid);
		query = purple_xmlnode_get_child(iq->node, "query");
		purple_xmlnode_set_attrib(query, "sid", jsx->stream_id);
		activate = purple_xmlnode_new_child(query, "activate");
		purple_xmlnode_insert_data(activate, purple_xfer_get_remote_user(xfer), -1);

		/* TODO: We need to wait for an activation result before starting */
	} else {
		PurpleXmlNode *query, *su;
		iq = jabber_iq_new_query(jsx->js, JABBER_IQ_RESULT, NS_BYTESTREAMS);
		purple_xmlnode_set_attrib(iq->node, "to", purple_xfer_get_remote_user(xfer));
		jabber_iq_set_id(iq, jsx->iq_id);
		query = purple_xmlnode_get_child(iq->node, "query");
		su = purple_xmlnode_new_child(query, "streamhost-used");
		purple_xmlnode_set_attrib(su, "jid", streamhost->jid);
	}

	jabber_iq_send(iq);

	jsx->local_streamhost_conn = G_SOCKET_CONNECTION(stream);
	socket = g_socket_connection_get_socket(jsx->local_streamhost_conn);
	purple_xfer_start(xfer, g_socket_get_fd(socket), NULL, -1);
}

static void
jabber_si_bytestreams_ibb_timeout_remove(JabberSIXfer *jsx)
{
	g_clear_handle_id(&jsx->ibb_timeout_handle, g_source_remove);
}

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

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

/* This is called when we connect to the SOCKS5 proxy server (through any
 * relevant account proxy)
 */
static void
jabber_si_bytestreams_socks5_connect_to_host_cb(GObject *source,
                                                GAsyncResult *result,
                                                gpointer user_data)
{
	PurpleXfer *xfer = PURPLE_XFER(user_data);
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	JabberID *dstjid;
	const JabberID *from_jid, *to_jid;
	GSocketConnection *conn;
	GProxy *proxy;
	GSocketAddress *addr;
	GInetSocketAddress *inet_addr;
	GSocketAddress *proxy_addr;
	gchar *dstaddr, *hash_input;
	GError *error = NULL;

	conn = g_socket_client_connect_to_host_finish(G_SOCKET_CLIENT(source),
	                                              result, &error);
	if (conn == NULL) {
		if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
			purple_debug_error("jabber", "Unable to connect to SOCKS5 host: %s",
			                   error->message);
			jabber_si_bytestreams_try_next_streamhost(
			        xfer, "Unable to connect to SOCKS5 host.");
		}

		g_clear_error(&error);
		return;
	}

	proxy = g_proxy_get_default_for_protocol("socks5");
	if (proxy == NULL) {
		purple_debug_error("jabber", "SOCKS5 proxy backend missing.");
		jabber_si_bytestreams_try_next_streamhost(
		        xfer, "SOCKS5 proxy backend missing.");
		g_object_unref(conn);
		return;
	}

	addr = g_socket_connection_get_remote_address(conn, &error);
	if (addr == NULL) {
		purple_debug_error(
		        "jabber",
		        "Unable to retrieve SOCKS5 host address from connection: %s",
		        error->message);
		jabber_si_bytestreams_try_next_streamhost(
		        xfer, "Unable to retrieve SOCKS5 host address from connection");
		g_object_unref(conn);
		g_object_unref(proxy);
		g_clear_error(&error);
		return;
	}

	dstjid = jabber_id_new(purple_xfer_get_remote_user(xfer));

	/* unknown file transfer type is assumed to be RECEIVE */
	if (purple_xfer_get_xfer_type(xfer) == PURPLE_XFER_TYPE_SEND) {
		from_jid = jsx->js->user;
		to_jid = dstjid;
	} else {
		from_jid = dstjid;
		to_jid = jsx->js->user;
	}

	/* Per XEP-0065, the 'host' must be SHA1(SID + from JID + to JID) */
	hash_input = g_strdup_printf("%s%s@%s/%s%s@%s/%s", jsx->stream_id,
	                             from_jid->node, from_jid->domain,
	                             from_jid->resource, to_jid->node,
	                             to_jid->domain, to_jid->resource);
	dstaddr = g_compute_checksum_for_string(G_CHECKSUM_SHA1, hash_input, -1);
	g_free(hash_input);
	jabber_id_free(dstjid);

	inet_addr = G_INET_SOCKET_ADDRESS(addr);

	proxy_addr =
	        g_proxy_address_new(g_inet_socket_address_get_address(inet_addr),
	                            g_inet_socket_address_get_port(inet_addr),
	                            "socks5", dstaddr, 0, NULL, NULL);
	g_object_unref(inet_addr);

	purple_debug_info("jabber", "Connecting to %s using SOCKS5 proxy", dstaddr);

	g_proxy_connect_async(proxy, G_IO_STREAM(conn), G_PROXY_ADDRESS(proxy_addr),
	                      jsx->cancellable, jabber_si_bytestreams_connect_cb,
	                      user_data);

	g_object_unref(proxy_addr);
	g_object_unref(conn);
	g_object_unref(proxy);
	g_free(dstaddr);
}

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

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

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

		purple_xmlnode_set_attrib(iq->node, "to", purple_xfer_get_remote_user(xfer));
		error = purple_xmlnode_new_child(iq->node, "error");
		purple_xmlnode_set_attrib(error, "code", "404");
		purple_xmlnode_set_attrib(error, "type", "cancel");
		inf = purple_xmlnode_new_child(error, "item-not-found");
		purple_xmlnode_set_namespace(inf, 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(jsx->stream_method & STREAM_METHOD_BYTESTREAMS) {
				jsx->stream_method &= ~STREAM_METHOD_BYTESTREAMS;
			}
			/* 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_xfer_type(xfer) == PURPLE_XFER_TYPE_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 = g_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;
	dstjid = jabber_id_new(purple_xfer_get_remote_user(xfer));

	/* TODO: Deal with zeroconf */

	if(dstjid != NULL && streamhost->host && streamhost->port > 0) {
		GError *error = NULL;
		PurpleAccount *account;

		account = purple_connection_get_account(jsx->js->gc);
		jsx->client = purple_gio_socket_client_new(account, &error);
		if (jsx->client != NULL) {
			if (purple_xfer_get_xfer_type(xfer) != PURPLE_XFER_TYPE_SEND) {
				/* When selecting a streamhost, timeout after
				 * STREAMHOST_CONNECT_TIMEOUT seconds, otherwise it takes
				 * forever
				 */
				g_socket_client_set_timeout(jsx->client,
				                            STREAMHOST_CONNECT_TIMEOUT);
			}

			purple_debug_info("jabber",
			                  "Connecting to SOCKS5 streamhost proxy %s:%d",
			                  streamhost->host, streamhost->port);

			g_socket_client_connect_to_host_async(
			        jsx->client, streamhost->host, streamhost->port,
			        jsx->cancellable,
			        jabber_si_bytestreams_socks5_connect_to_host_cb, xfer);
		} else {
			purple_debug_error(
			        "jabber",
			        "Failed to connect to SOCKS5 streamhost proxy: %s",
			        error->message);
			g_clear_error(&error);
		}

		jabber_id_free(dstjid);
	}

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

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

	if(type != JABBER_IQ_SET)
		return;

	if(!from)
		return;

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

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

	jsx = JABBER_SI_XFER(xfer);

	if(!jsx->accepted)
		return;

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

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

		if((jid = purple_xmlnode_get_attrib(streamhost, "jid")) &&
				((zeroconf = purple_xmlnode_get_attrib(streamhost, "zeroconf")) ||
				((host = purple_xmlnode_get_attrib(streamhost, "host")) &&
				(port = purple_xmlnode_get_attrib(streamhost, "port")) &&
				(portnum = atoi(port))))) {
			/* ignore 0.0.0.0 */
			if(purple_strequal(host, "0.0.0.0") == FALSE) {
				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_write_response_cb(GObject *source,
                                                  GAsyncResult *result,
                                                  gpointer user_data)
{
	PurpleXfer *xfer = PURPLE_XFER(user_data);
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	gsize bytes_written = 0;
	GError *error = NULL;

	if (!g_output_stream_write_all_finish(G_OUTPUT_STREAM(source), result,
	                                      &bytes_written, &error)) {
		if (error->code != G_IO_ERROR_CANCELLED) {
			purple_xfer_cancel_remote(xfer);
		}
		g_clear_error(&error);
	} else {
		/* 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.");
	}

	g_clear_pointer(&jsx->socks_buf, g_free);
}

static void
jabber_si_xfer_bytestreams_send_read_request_cb(GObject *source,
                                                GAsyncResult *result,
                                                gpointer user_data)
{
	PurpleXfer *xfer = PURPLE_XFER(user_data);
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	gsize bytes_read = 0;
	GError *error = NULL;
	GOutputStream *output = NULL;
	gsize bufsize;
	gchar *dstaddr, *hash;
	gchar *host;

	purple_debug_info("jabber",
	                  "in jabber_si_xfer_bytestreams_send_read_request_cb");

	if (!g_input_stream_read_all_finish(G_INPUT_STREAM(source), result,
	                                    &bytes_read, &error)) {
		if (error->code != G_IO_ERROR_CANCELLED) {
			purple_xfer_cancel_remote(xfer);
		}
		g_clear_error(&error);
		return;
	}

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

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

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

	g_free(hash);
	g_free(dstaddr);

	g_free(jsx->socks_buf);
	host = purple_network_get_my_ip_from_gio(
	        G_SOCKET_CONNECTION(jsx->js->stream));

	bufsize = 5 + strlen(host) + 2;
	jsx->socks_buf = g_malloc(bufsize);

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

	output = g_io_stream_get_output_stream(
	        G_IO_STREAM(jsx->local_streamhost_conn));
	g_output_stream_write_all_async(
	        output, jsx->socks_buf, bufsize, G_PRIORITY_DEFAULT,
	        jsx->cancellable, jabber_si_xfer_bytestreams_send_write_response_cb,
	        xfer);

	g_free(host);
}

static void
jabber_si_xfer_bytestreams_send_read_request_header_cb(GObject *source,
                                                       GAsyncResult *result,
                                                       gpointer user_data)
{
	GInputStream *input = G_INPUT_STREAM(source);
	PurpleXfer *xfer = PURPLE_XFER(user_data);
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	gsize bytes_read = 0;
	GError *error = NULL;

	purple_debug_info(
	        "jabber",
	        "in jabber_si_xfer_bytestreams_send_read_request_header_cb");

	if (!g_input_stream_read_all_finish(input, result, &bytes_read, &error)) {
		if (error->code != G_IO_ERROR_CANCELLED) {
			purple_xfer_cancel_remote(xfer);
		}
		g_clear_error(&error);
		return;
	}

	if (jsx->socks_buf[0] != 0x05 || jsx->socks_buf[1] != 0x01 ||
	    jsx->socks_buf[3] != 0x03 || jsx->socks_buf[4] != 40) {
		purple_debug_info(
		        "jabber",
		        "Invalid socks5 conn req. header[0x%x,0x%x,0x%x,0x%x,0x%x]",
		        jsx->socks_buf[0], jsx->socks_buf[1], jsx->socks_buf[2],
		        jsx->socks_buf[3], jsx->socks_buf[4]);
		purple_xfer_cancel_remote(xfer);
		return;
	}

	/* Read DST.ADDR and the following 2-byte port number. */
	purple_debug_info("jabber", "reading %u bytes for DST.ADDR + port num",
	                  jsx->socks_buf[4] + 2);
	g_input_stream_read_all_async(
	        input, jsx->socks_buf + 5, jsx->socks_buf[4] + 2,
	        G_PRIORITY_DEFAULT, jsx->cancellable,
	        jabber_si_xfer_bytestreams_send_read_request_cb, xfer);
}

static void
jabber_si_xfer_bytestreams_send_write_method_cb(GObject *source,
                                                GAsyncResult *result,
                                                gpointer user_data)
{
	PurpleXfer *xfer = PURPLE_XFER(user_data);
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	gsize bytes_written = 0;
	GError *error = NULL;

	if (!g_output_stream_write_all_finish(G_OUTPUT_STREAM(source), result,
	                                      &bytes_written, &error)) {
		if (error->code != G_IO_ERROR_CANCELLED) {
			purple_xfer_cancel_remote(xfer);
		}
		g_clear_pointer(&jsx->socks_buf, g_free);
		g_clear_error(&error);
		return;
	}

	/* If we sent a "Success", wait for a response, otherwise give up and cancel */
	if (jsx->socks_buf[1] == 0x00) {
		GInputStream *input = g_io_stream_get_input_stream(
		        G_IO_STREAM(jsx->local_streamhost_conn));

		g_free(jsx->socks_buf);
		/* Request is: Version, Command, Reserved, Address type, Address length,
		 * Address (up to 255), 2-byte port */
		jsx->socks_buf = g_malloc(5 + 255 + 2);
		g_input_stream_read_all_async(
		        input, jsx->socks_buf, 5, G_PRIORITY_DEFAULT, jsx->cancellable,
		        jabber_si_xfer_bytestreams_send_read_request_header_cb, xfer);
	} else {
		purple_xfer_cancel_remote(xfer);
	}
}

static void
jabber_si_xfer_bytestreams_send_read_methods_cb(GObject *source,
                                                GAsyncResult *result,
                                                gpointer user_data)
{
	PurpleXfer *xfer = PURPLE_XFER(user_data);
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	gsize bytes_read = 0;
	GError *error = NULL;
	GOutputStream *output;
	gboolean valid_method_found = FALSE;
	gint i;

	purple_debug_info("jabber",
	                  "in jabber_si_xfer_bytestreams_send_read_methods_cb");

	if (!g_input_stream_read_all_finish(G_INPUT_STREAM(source), result,
	                                    &bytes_read, &error)) {
		if (error->code != G_IO_ERROR_CANCELLED) {
			purple_xfer_cancel_remote(xfer);
		}
		g_clear_error(&error);
		return;
	}

	purple_debug_info("jabber", "going to test %u different methods",
	                  jsx->socks_buf[1]);

	for (i = 0; i < jsx->socks_buf[1]; i++) {
		purple_debug_info("jabber", "testing %u", jsx->socks_buf[i + 2]);
		if (jsx->socks_buf[i + 2] == 0x00) {
			valid_method_found = TRUE;
			break;
		}
	}

	g_free(jsx->socks_buf);
	jsx->socks_buf = g_malloc(2);
	jsx->socks_buf[0] = 0x05;
	jsx->socks_buf[1] = valid_method_found ? 0x00 : 0xFF;
	output = g_io_stream_get_output_stream(
	        G_IO_STREAM(jsx->local_streamhost_conn));
	g_output_stream_write_all_async(
	        output, jsx->socks_buf, 2, G_PRIORITY_DEFAULT, jsx->cancellable,
	        jabber_si_xfer_bytestreams_send_write_method_cb, xfer);
}

static void
jabber_si_xfer_bytestreams_send_read_version_cb(GObject *source,
                                                GAsyncResult *result,
                                                gpointer user_data)
{
	GInputStream *input = G_INPUT_STREAM(source);
	PurpleXfer *xfer = PURPLE_XFER(user_data);
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	gsize bytes_read = 0;
	GError *error = NULL;

	purple_debug_info("jabber",
	                  "in jabber_si_xfer_bytestreams_send_read_version_cb");

	/* Try to read the SOCKS5 header */
	if (!g_input_stream_read_all_finish(input, result, &bytes_read, &error)) {
		if (error->code != G_IO_ERROR_CANCELLED) {
			purple_xfer_cancel_remote(xfer);
		}
		g_clear_error(&error);
		return;
	}

	purple_debug_info("jabber", "checking to make sure we're socks FIVE");
	if (jsx->socks_buf[0] != 0x05) {
		purple_xfer_cancel_remote(xfer);
		return;
	}

	/* Number of methods is given in second byte. */
	purple_debug_info("jabber", "reading %u bytes for auth methods",
	                  jsx->socks_buf[1]);
	g_input_stream_read_all_async(
	        input, jsx->socks_buf + 2, jsx->socks_buf[1], G_PRIORITY_DEFAULT,
	        jsx->cancellable, jabber_si_xfer_bytestreams_send_read_methods_cb,
	        xfer);
}

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(G_GNUC_UNUSED GSocketService *service,
                                             GSocketConnection *connection,
                                             GObject *source_object,
                                             G_GNUC_UNUSED gpointer data)
{
	PurpleXfer *xfer = PURPLE_XFER(source_object);
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	GInputStream *input = NULL;

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

	jsx->local_streamhost_conn = g_object_ref(connection);
	g_socket_service_stop(jsx->service);
	g_clear_object(&jsx->service);

	/* Initial message is: Version, Method count, up to 255 methods. */
	jsx->socks_buf = g_malloc(2 + 255);
	input = g_io_stream_get_input_stream(
	        G_IO_STREAM(jsx->local_streamhost_conn));
	g_input_stream_read_all_async(
	        input, jsx->socks_buf, 2, G_PRIORITY_DEFAULT, jsx->cancellable,
	        jabber_si_xfer_bytestreams_send_read_version_cb, xfer);
}

static void
jabber_si_connect_proxy_cb(JabberStream *js, const char *from,
                           JabberIqType type, G_GNUC_UNUSED const char *id,
                           PurpleXmlNode *packet, gpointer data)
{
	PurpleXfer *xfer = data;
	JabberSIXfer *jsx;
	PurpleXmlNode *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;
	}

	jsx = JABBER_SI_XFER(xfer);

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

	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) {
			/* if we previously tried bytestreams, we need to disable it. */
			if(jsx->stream_method & STREAM_METHOD_BYTESTREAMS) {
				jsx->stream_method &= ~STREAM_METHOD_BYTESTREAMS;
			}

			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_xfer_type(xfer) == PURPLE_XFER_TYPE_SEND
				&& !jsx->ibb_session) {
				jabber_si_xfer_ibb_send_init(js, xfer);
			} else {
				jsx->ibb_timeout_handle = g_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 = purple_xmlnode_get_child(packet, "query")))
		return;

	if(!(streamhost_used = purple_xmlnode_get_child(query, "streamhost-used")))
		return;

	if(!(jid = purple_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 (purple_strequal(jid, my_jid)) {
			GSocket *sock = NULL;
			gint fd = -1;
			purple_debug_info("jabber", "Got local SOCKS5 streamhost-used.\n");
			sock = g_socket_connection_get_socket(jsx->local_streamhost_conn);
			fd = g_socket_get_fd(sock);
			_purple_network_set_common_socket_flags(fd);
			purple_xfer_start(xfer, fd, NULL, -1);
		} else {
			/* if available, try to revert to IBB... */
			if (jsx->stream_method & STREAM_METHOD_IBB) {
				if(jsx->stream_method & STREAM_METHOD_BYTESTREAMS) {
					jsx->stream_method &= ~STREAM_METHOD_BYTESTREAMS;
				}

				purple_debug_info("jabber",
					"jabber_si_connect_proxy_cb: trying to revert to IBB\n");
				if (purple_xfer_get_xfer_type(xfer) == PURPLE_XFER_TYPE_SEND) {
					jabber_si_xfer_ibb_send_init(jsx->js, xfer);
				} else {
					jsx->ibb_timeout_handle = g_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.*/
	g_clear_object(&jsx->local_streamhost_conn);
	if (jsx->service) {
		g_socket_service_stop(jsx->service);
		g_clear_object(&jsx->service);
	}

	jsx->streamhosts = g_list_remove_link(jsx->streamhosts, matched);
	g_list_free_full(jsx->streamhosts, (GDestroyNotify)jabber_bytestreams_streamhost_free);

	jsx->streamhosts = matched;

	jabber_si_bytestreams_attempt_connect(xfer);
}

static void
jabber_si_xfer_bytestreams_send_local_info(PurpleXfer *xfer, GList *local_ips)
{
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	JabberIq *iq;
	PurpleXmlNode *query, *streamhost;
	char port[6];
	GList *tmp;
	JabberBytestreamsStreamhost *sh, *sh2;
	int streamhost_count = 0;

	iq = jabber_iq_new_query(jsx->js, JABBER_IQ_SET, NS_BYTESTREAMS);
	purple_xmlnode_set_attrib(iq->node, "to", purple_xfer_get_remote_user(xfer));
	query = purple_xmlnode_get_child(iq->node, "query");

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

	/* If we successfully started listening locally */
	if (local_ips) {
		gchar *jid;

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

		/* Include the localhost's IPs (for in-network transfers) */
		while (local_ips) {
			gchar *local_ip = local_ips->data;
			streamhost_count++;
			streamhost = purple_xmlnode_new_child(query, "streamhost");
			purple_xmlnode_set_attrib(streamhost, "jid", jid);
			purple_xmlnode_set_attrib(streamhost, "host", local_ip);
			purple_xmlnode_set_attrib(streamhost, "port", port);
			g_free(local_ip);
			local_ips = g_list_delete_link(local_ips, local_ips);
		}

		g_free(jid);
	}

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

		/* TODO: deal with zeroconf proxies */

		if (!sh->jid) {
			continue;
		} else if (!sh->host) {
			continue;
		} else if (sh->port <= 0) {
			continue;
		} else if (g_list_find_custom(jsx->streamhosts, sh->jid,
		                              jabber_si_compare_jid) != NULL) {
			continue;
		}

		streamhost_count++;
		streamhost = purple_xmlnode_new_child(query, "streamhost");
		purple_xmlnode_set_attrib(streamhost, "jid", sh->jid);
		purple_xmlnode_set_attrib(streamhost, "host", sh->host);
		g_snprintf(port, sizeof(port), "%hu", sh->port);
		purple_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) {
			if(jsx->stream_method & STREAM_METHOD_BYTESTREAMS) {
				jsx->stream_method &= ~STREAM_METHOD_BYTESTREAMS;
			}
			purple_debug_info("jabber", "jabber_si_xfer_bytestreams_send_local_"
			                            "info: trying to revert to IBB\n");
			if (purple_xfer_get_xfer_type(xfer) == PURPLE_XFER_TYPE_SEND) {
				/* if we are the sender, init the IBB session... */
				jabber_si_xfer_ibb_send_init(jsx->js, xfer);
			} else {
				jsx->ibb_timeout_handle = g_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 = JABBER_SI_XFER(xfer);
	PurpleProxyType proxy_type;
	GList *local_ips = NULL;

	/* TODO: This should probably be done with an account option instead of
	 *       piggy-backing on the TOR proxy type. */
	proxy_type = purple_proxy_info_get_proxy_type(
	        purple_proxy_get_setup(purple_connection_get_account(jsx->js->gc)));
	if (proxy_type == PURPLE_PROXY_TYPE_TOR) {
		purple_debug_info("jabber", "Skipping attempting local streamhost.\n");
		jsx->service = NULL;
	} else {
		guint16 port = 0;
		GError *error = NULL;
		jsx->service = g_socket_service_new();
		port = purple_socket_listener_add_any_inet_port(
		        G_SOCKET_LISTENER(jsx->service), G_OBJECT(xfer), &error);
		if (port != 0) {
			purple_xfer_set_local_port(xfer, port);
		} else {
			purple_debug_error("jabber",
			                   "Unable to create streamhost socket listener: "
			                   "%s. Trying proxy instead.",
			                   error->message);
			g_error_free(error);
			g_clear_object(&jsx->service);
		}
	}

	if (jsx->service) {
		gchar *public_ip;

		/* Include the public IP (assuming there is a port mapped somehow) */
		public_ip = purple_network_get_my_ip_from_gio(
		        G_SOCKET_CONNECTION(jsx->js->stream));
		if (!purple_strequal(public_ip, "0.0.0.0")) {
			local_ips = g_list_append(local_ips, public_ip);
		} else {
			g_free(public_ip);
		}

		g_signal_connect(
		        G_OBJECT(jsx->service), "incoming",
		        G_CALLBACK(jabber_si_xfer_bytestreams_send_connected_cb), NULL);
	}

	jabber_si_xfer_bytestreams_send_local_info(xfer, local_ips);
}

static void
jabber_si_xfer_ibb_error_cb(JabberIBBSession *sess)
{
	PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess);

	purple_debug_error("jabber", "an error occurred during IBB file 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);

	purple_debug_info("jabber", "the remote user closed the transfer\n");
	if (purple_xfer_get_bytes_remaining(xfer) > 0) {
		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 = JABBER_SI_XFER(xfer);

	if ((goffset)size <= purple_xfer_get_bytes_remaining(xfer)) {
		purple_debug_info("jabber", "about to write %" G_GSIZE_FORMAT " bytes from IBB stream\n",
			size);
		purple_circular_buffer_append(jsx->ibb_buffer, data, size);
		purple_xfer_protocol_ready(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 gssize
jabber_si_xfer_ibb_read(PurpleXfer *xfer, guchar **out_buffer, size_t buf_size)
{
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	guchar *buffer;
	gsize size;
	gsize tmp;

	if(jsx->stream_method & STREAM_METHOD_BYTESTREAMS) {
		return PURPLE_XFER_CLASS(jabber_si_xfer_parent_class)->read(xfer, out_buffer, buf_size);
	}

	size = purple_circular_buffer_get_used(jsx->ibb_buffer);

	*out_buffer = buffer = g_malloc(size);
	while ((tmp = purple_circular_buffer_get_max_read(jsx->ibb_buffer))) {
		const gchar *output = purple_circular_buffer_get_output(jsx->ibb_buffer);
		memcpy(buffer, output, tmp);
		buffer += tmp;
		purple_circular_buffer_mark_read(jsx->ibb_buffer, tmp);
	}

	return size;
}

static gboolean
jabber_si_xfer_ibb_open_cb(JabberStream *js, const char *who, const char *id,
                           PurpleXmlNode *open)
{
	const gchar *sid = purple_xmlnode_get_attrib(open, "sid");
	PurpleXfer *xfer = jabber_si_xfer_find(js, sid, who);

	if (xfer) {
		JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
		JabberIBBSession *sess =
			jabber_ibb_session_create_from_xmlnode(js, who, id, open, xfer);

		jabber_si_bytestreams_ibb_timeout_remove(jsx);

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

			jsx->ibb_session = sess;
			/* we handle up to block-size bytes of decoded data, to handle
			 clients interpreting the block-size attribute as that
			 (see also remark in ibb.c) */
			jsx->ibb_buffer =
				purple_circular_buffer_new(jabber_ibb_session_get_block_size(sess));

			/* start the transfer */
			purple_xfer_start(xfer, -1, 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 gssize
jabber_si_xfer_ibb_write(PurpleXfer *xfer, const guchar *buffer, size_t len)
{
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	JabberIBBSession *sess = jsx->ibb_session;
	gsize packet_size;

	if(jsx->stream_method & STREAM_METHOD_BYTESTREAMS) {
		purple_debug_error("jabber", "falling back to raw socket\n");
		return PURPLE_XFER_CLASS(jabber_si_xfer_parent_class)->write(xfer, buffer, len);
	}

	packet_size = MIN(len, jabber_ibb_session_get_max_data_size(sess));

	jabber_ibb_session_send_data(sess, buffer, packet_size);

	return packet_size;
}

static void
jabber_si_xfer_ibb_sent_cb(JabberIBBSession *sess)
{
	PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess);
	goffset 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... */
		purple_xfer_protocol_ready(xfer);
	}
}

static void
jabber_si_xfer_ibb_opened_cb(JabberIBBSession *sess)
{
	PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess);

	if (jabber_ibb_session_get_state(sess) == JABBER_IBB_SESSION_OPENED) {
		purple_xfer_start(xfer, -1, NULL, 0);
		purple_xfer_protocol_ready(xfer);
	} else {
		/* error */
		purple_xfer_end(xfer);
	}
}

static void
jabber_si_xfer_ibb_send_init(JabberStream *js, PurpleXfer *xfer)
{
	JabberSIXfer *jsx = JABBER_SI_XFER(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);

		jsx->ibb_buffer =
			purple_circular_buffer_new(jabber_ibb_session_get_max_data_size(jsx->ibb_session));

		/* 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, G_GNUC_UNUSED const char *from,
                              G_GNUC_UNUSED JabberIqType type,
                              G_GNUC_UNUSED const char *id,
                              PurpleXmlNode *packet, gpointer data)
{
	PurpleXfer *xfer = data;
	PurpleXmlNode *si, *feature, *x, *field, *value;
	gboolean found_method = FALSE;

	if(!(si = purple_xmlnode_get_child_with_namespace(packet, "si", "http://jabber.org/protocol/si"))) {
		purple_xfer_cancel_remote(xfer);
		return;
	}

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

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

	for(field = purple_xmlnode_get_child(x, "field"); field; field = purple_xmlnode_get_next_twin(field)) {
		const char *var = purple_xmlnode_get_attrib(field, "var");
		JabberSIXfer *jsx = JABBER_SI_XFER(xfer);

		if(purple_strequal(var, "stream-method")) {
			if((value = purple_xmlnode_get_child(field, "value"))) {
				char *val = purple_xmlnode_get_data(value);
				if(purple_strequal(val, NS_BYTESTREAMS)) {
					jabber_si_xfer_bytestreams_send_init(xfer);
					jsx->stream_method |= STREAM_METHOD_BYTESTREAMS;
					found_method = TRUE;
				} else if (purple_strequal(val, NS_IBB)) {
					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 = JABBER_SI_XFER(xfer);
	JabberIq *iq;
	PurpleXmlNode *si, *file, *feature, *x, *field, *option, *value;
	char buf[32];
#if ENABLE_FT_THUMBNAILS
	gconstpointer thumb;
	gsize thumb_size;

	purple_xfer_prepare_thumbnail(xfer, "jpeg,png");
#endif
	purple_xfer_set_filename(xfer, g_path_get_basename(purple_xfer_get_local_filename(xfer)));

	iq = jabber_iq_new(jsx->js, JABBER_IQ_SET);
	purple_xmlnode_set_attrib(iq->node, "to", purple_xfer_get_remote_user(xfer));
	si = purple_xmlnode_new_child(iq->node, "si");
	purple_xmlnode_set_namespace(si, "http://jabber.org/protocol/si");
	jsx->stream_id = jabber_get_next_id(jsx->js);
	purple_xmlnode_set_attrib(si, "id", jsx->stream_id);
	purple_xmlnode_set_attrib(si, "profile", NS_SI_FILE_TRANSFER);

	file = purple_xmlnode_new_child(si, "file");
	purple_xmlnode_set_namespace(file, NS_SI_FILE_TRANSFER);
	purple_xmlnode_set_attrib(file, "name", purple_xfer_get_filename(xfer));
	g_snprintf(buf, sizeof(buf), "%" G_GOFFSET_FORMAT, purple_xfer_get_size(xfer));
	purple_xmlnode_set_attrib(file, "size", buf);
	/* maybe later we'll do hash and date attribs */

#if ENABLE_FT_THUMBNAILS
	/* add thumbnail, if appropriate */
	if ((thumb = purple_xfer_get_thumbnail(xfer, &thumb_size))) {
		const gchar *mimetype = purple_xfer_get_thumbnail_mimetype(xfer);
		JabberData *thumbnail_data =
			jabber_data_create_from_data(thumb, thumb_size,
				mimetype, TRUE, jsx->js);
		PurpleXmlNode *thumbnail = purple_xmlnode_new_child(file, "thumbnail");
		purple_xmlnode_set_namespace(thumbnail, NS_THUMBS);
		purple_xmlnode_set_attrib(thumbnail, "cid",
			jabber_data_get_cid(thumbnail_data));
		purple_xmlnode_set_attrib(thumbnail, "mime-type", mimetype);
		/* cache data */
		jabber_data_associate_local(thumbnail_data, NULL);
	}
#endif

	feature = purple_xmlnode_new_child(si, "feature");
	purple_xmlnode_set_namespace(feature, "http://jabber.org/protocol/feature-neg");
	x = purple_xmlnode_new_child(feature, "x");
	purple_xmlnode_set_namespace(x, "jabber:x:data");
	purple_xmlnode_set_attrib(x, "type", "form");
	field = purple_xmlnode_new_child(x, "field");
	purple_xmlnode_set_attrib(field, "var", "stream-method");
	purple_xmlnode_set_attrib(field, "type", "list-single");
	/* maybe we should add an option to always skip bytestreams for people
		behind troublesome firewalls */
	option = purple_xmlnode_new_child(field, "option");
	value = purple_xmlnode_new_child(option, "value");
	purple_xmlnode_insert_data(value, NS_BYTESTREAMS, -1);
	option = purple_xmlnode_new_child(field, "option");
	value = purple_xmlnode_new_child(option, "value");
	purple_xmlnode_insert_data(value, NS_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);
}

/*
 * 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 = JABBER_SI_XFER(xfer);

	/* if there is an IBB session active, send close on that */
	if (jsx->ibb_session) {
		jabber_ibb_session_close(jsx->ibb_session);
	}
	purple_debug_info("jabber", "in jabber_si_xfer_cancel_send\n");

	g_cancellable_cancel(jsx->cancellable);
}


static void jabber_si_xfer_request_denied(PurpleXfer *xfer)
{
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	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;
		PurpleXmlNode *error, *child;
		iq = jabber_iq_new(js, JABBER_IQ_ERROR);
		purple_xmlnode_set_attrib(iq->node, "to", purple_xfer_get_remote_user(xfer));
		jabber_iq_set_id(iq, jsx->iq_id);

		error = purple_xmlnode_new_child(iq->node, "error");
		purple_xmlnode_set_attrib(error, "type", "cancel");
		child = purple_xmlnode_new_child(error, "forbidden");
		purple_xmlnode_set_namespace(child, NS_XMPP_STANZAS);
		child = purple_xmlnode_new_child(error, "text");
		purple_xmlnode_set_namespace(child, NS_XMPP_STANZAS);
		purple_xmlnode_insert_data(child, "Offer Declined", -1);

		jabber_iq_send(iq);
	}

	purple_debug_info("jabber", "in jabber_si_xfer_request_denied\n");
}


static void jabber_si_xfer_cancel_recv(PurpleXfer *xfer)
{
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	/* if there is an IBB session active, send close */
	if (jsx->ibb_session) {
		jabber_ibb_session_close(jsx->ibb_session);
	}
	purple_debug_info("jabber", "in jabber_si_xfer_cancel_recv\n");

	g_cancellable_cancel(jsx->cancellable);
}


static void jabber_si_xfer_send_disco_cb(JabberStream *js, const char *who,
		JabberCapabilities capabilities, gpointer data)
{
	PurpleXfer *xfer = PURPLE_XFER(data);
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);

	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,
			purple_request_cpar_from_connection(js->gc));
		g_free(msg);
		purple_xfer_cancel_local(xfer);
	}
}

static void
resource_select_cancel_cb(PurpleXfer *xfer,
                          G_GNUC_UNUSED PurpleRequestPage *page)
{
	purple_xfer_cancel_local(xfer);
}

static void do_transfer_send(PurpleXfer *xfer, const char *resource)
{
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	char **who_v = g_strsplit(purple_xfer_get_remote_user(xfer), "/", 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);
	purple_xfer_set_remote_user(xfer, who);

	if (jbr && jabber_resource_know_capabilities(jbr)) {
		char *msg;

		if (jabber_resource_has_capability(jbr, NS_IBB))
			jsx->stream_method |= STREAM_METHOD_IBB;
		if (jabber_resource_has_capability(jbr, NS_SI_FILE_TRANSFER)) {
			jabber_si_xfer_send_request(xfer);
			g_free(who);
			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,
			purple_request_cpar_from_connection(jsx->js->gc));
		g_free(msg);
		purple_xfer_cancel_local(xfer);
	} else {
		jabber_disco_info_do(jsx->js, who,
				jabber_si_xfer_send_disco_cb, xfer);
	}

	g_free(who);
}

static void
resource_select_ok_cb(PurpleXfer *xfer, PurpleRequestPage *page)
{
	const char *selected_label = purple_request_page_get_choice(page, "resource");

	do_transfer_send(xfer, selected_label);
}

static void jabber_si_xfer_xfer_init(PurpleXfer *xfer)
{
	JabberSIXfer *jsx = JABBER_SI_XFER(xfer);
	JabberIq *iq;
	if(purple_xfer_get_xfer_type(xfer) == PURPLE_XFER_TYPE_SEND) {
		JabberBuddy *jb;
		JabberBuddyResource *jbr = NULL;
		char *resource;
		GList *resources = NULL;

		if(NULL != (resource = jabber_get_resource(purple_xfer_get_remote_user(xfer)))) {
			/* 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, purple_xfer_get_remote_user(xfer), TRUE);

		if (jb) {
			GList *l;

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

				if (!jabber_resource_know_capabilities(jbr) ||
				    (jabber_resource_has_capability(jbr, NS_SI_FILE_TRANSFER)
				     && (jabber_resource_has_capability(jbr, NS_BYTESTREAMS)
				         || jabber_resource_has_capability(jbr, NS_IBB)))) {
					resources = g_list_append(resources, jbr);
				}
			}
		}

		if (!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"), purple_xfer_get_remote_user(xfer));
			} else if(jb->subscription & JABBER_SUB_TO) {
				msg = g_strdup_printf(_("Unable to send file to %s, user is not online"), purple_xfer_get_remote_user(xfer));
			} else {
				msg = g_strdup_printf(_("Unable to send file to %s, not subscribed to user presence"), purple_xfer_get_remote_user(xfer));
			}

			purple_notify_error(jsx->js->gc, _("File Send Failed"),
				_("File Send Failed"), msg,
				purple_request_cpar_from_connection(jsx->js->gc));
			g_free(msg);
		} else if (g_list_length(resources) == 1) {
			/* only 1 resource online (probably our most common case)
			 * so no need to ask who to send to */
			jbr = resources->data;
			do_transfer_send(xfer, jbr->name);
		} else {
			/* we've got multiple resources, we need to pick one to send to */
			GList *l;
			PurpleRequestPage *page = NULL;
			PurpleRequestField *field = NULL;
			PurpleRequestFieldChoice *choice = NULL;
			PurpleRequestGroup *group = NULL;
			char *msg = NULL;

			field = purple_request_field_choice_new("resource", _("Resource"), 0);
			choice = PURPLE_REQUEST_FIELD_CHOICE(field);
			for(l = resources; l; l = l->next) {
				jbr = l->data;
				purple_request_field_choice_add_full(choice, jbr->name,
				                                     g_strdup(jbr->name),
				                                     g_free);
			}

			group = purple_request_group_new(NULL);
			purple_request_group_add_field(group, field);

			page = purple_request_page_new();
			purple_request_page_add_group(page, group);

			msg = g_strdup_printf(_("Please select the resource of %s to "
			                        "which you would like to send a file"),
			                      purple_xfer_get_remote_user(xfer));

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

			g_free(msg);
		}

		g_list_free(resources);
	} else {
		PurpleXmlNode *si, *feature, *x, *field, *value;

		iq = jabber_iq_new(jsx->js, JABBER_IQ_RESULT);
		purple_xmlnode_set_attrib(iq->node, "to", purple_xfer_get_remote_user(xfer));
		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 = purple_xmlnode_new_child(iq->node, "si");
		purple_xmlnode_set_namespace(si, "http://jabber.org/protocol/si");

		feature = purple_xmlnode_new_child(si, "feature");
		purple_xmlnode_set_namespace(feature, "http://jabber.org/protocol/feature-neg");

		x = purple_xmlnode_new_child(feature, "x");
		purple_xmlnode_set_namespace(x, "jabber:x:data");
		purple_xmlnode_set_attrib(x, "type", "submit");
		field = purple_xmlnode_new_child(x, "field");
		purple_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 unnecessary 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 = purple_xmlnode_new_child(field, "value");
			purple_xmlnode_insert_data(value, NS_BYTESTREAMS, -1);
		} else if(jsx->stream_method & STREAM_METHOD_IBB) {
			value = purple_xmlnode_new_child(field, "value");
			purple_xmlnode_insert_data(value, NS_IBB, -1);
		}

		jabber_iq_send(iq);
	}
}

PurpleXfer *
jabber_si_new_xfer(G_GNUC_UNUSED PurpleProtocolXfer *prplxfer,
                   PurpleConnection *gc, const char *who)
{
	JabberStream *js;
	JabberSIXfer *jsx;

	js = purple_connection_get_protocol_data(gc);

	jsx = g_object_new(
		JABBER_TYPE_SI_XFER,
		"account", purple_connection_get_account(gc),
		"type", PURPLE_XFER_TYPE_SEND,
		"remote-user", who,
		NULL
	);

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

	return PURPLE_XFER(jsx);
}

void jabber_si_xfer_send(PurpleProtocolXfer *prplxfer, PurpleConnection *gc, const char *who, const char *file)
{
	PurpleXfer *xfer;

	xfer = jabber_si_new_xfer(prplxfer, gc, who);

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

#if ENABLE_FT_THUMBNAILS
static void
jabber_si_thumbnail_cb(JabberData *data, gchar *alt, gpointer userdata)
{
	PurpleXfer *xfer = (PurpleXfer *) userdata;

	if (data) {
		purple_xfer_set_thumbnail(xfer, jabber_data_get_data(data),
			jabber_data_get_size(data), jabber_data_get_type(data));
		/* data is ephemeral, get rid of now (the xfer re-owned the thumbnail */
		jabber_data_destroy(data);
	}

	purple_xfer_request(xfer);
}
#endif

static void
jabber_si_parse(JabberStream *js, const char *from,
                G_GNUC_UNUSED JabberIqType type, const char *id,
                PurpleXmlNode *si)
{
	JabberSIXfer *jsx;
	PurpleXmlNode *file, *feature, *x, *field, *option, *value;
#if ENABLE_FT_THUMBNAILS
	PurpleXmlNode *thumbnail;
#endif
	const char *stream_id, *filename, *filesize_c, *profile;
	goffset filesize = 0;

	if(!(profile = purple_xmlnode_get_attrib(si, "profile")) ||
			!purple_strequal(profile, NS_SI_FILE_TRANSFER))
		return;

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

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

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

	if((filesize_c = purple_xmlnode_get_attrib(file, "size")))
		filesize = g_ascii_strtoull(filesize_c, NULL, 10);

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

	if(!(x = purple_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(jabber_si_xfer_find(js, stream_id, from) != NULL)
		return;

	jsx = g_object_new(
		JABBER_TYPE_SI_XFER,
		"account", purple_connection_get_account(js->gc),
		"type", PURPLE_XFER_TYPE_RECEIVE,
		"remote-user", from,
		NULL
	);

	for(field = purple_xmlnode_get_child(x, "field"); field; field = purple_xmlnode_get_next_twin(field)) {
		const char *var = purple_xmlnode_get_attrib(field, "var");
		if(purple_strequal(var, "stream-method")) {
			for(option = purple_xmlnode_get_child(field, "option"); option;
					option = purple_xmlnode_get_next_twin(option)) {
				if((value = purple_xmlnode_get_child(option, "value"))) {
					char *val;
					if((val = purple_xmlnode_get_data(value))) {
						if(purple_strequal(val, NS_BYTESTREAMS)) {
							jsx->stream_method |= STREAM_METHOD_BYTESTREAMS;
						} else if(purple_strequal(val, NS_IBB)) {
							jsx->stream_method |= STREAM_METHOD_IBB;
						}
						g_free(val);
					}
				}
			}
		}
	}

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

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

	purple_xfer_set_filename(PURPLE_XFER(jsx), filename);
	if(filesize > 0) {
		purple_xfer_set_size(PURPLE_XFER(jsx), filesize);
	}

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

#if ENABLE_FT_THUMBNAILS
	/* if there is a thumbnail, we should request it... */
	if ((thumbnail = purple_xmlnode_get_child_with_namespace(file, "thumbnail",
		NS_THUMBS))) {
		const char *cid = purple_xmlnode_get_attrib(thumbnail, "cid");
		if (cid) {
			jabber_data_request(js, cid, purple_xfer_get_remote_user(PURPLE_XFER(jsx)),
			    NULL, TRUE, jabber_si_thumbnail_cb, jsx);
			return;
		}
	}
#endif

	purple_xfer_request(PURPLE_XFER(jsx));
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
static void
jabber_si_xfer_init(JabberSIXfer *jsx)
{
	jsx->ibb_session = NULL;
	jsx->cancellable = g_cancellable_new();
}

static void
jabber_si_xfer_finalize(GObject *obj) {
	JabberSIXfer *jsx = JABBER_SI_XFER(obj);
	JabberStream *js = jsx->js;

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

	g_cancellable_cancel(jsx->cancellable);
	g_clear_object(&jsx->cancellable);

	g_clear_object(&jsx->client);
	g_clear_object(&jsx->local_streamhost_conn);
	if (jsx->service) {
		g_socket_service_stop(jsx->service);
	}
	g_clear_object(&jsx->service);

	if (jsx->iq_id != NULL) {
		jabber_iq_remove_callback_by_id(js, jsx->iq_id);
	}

	g_clear_handle_id(&jsx->connect_timeout, g_source_remove);
	g_clear_handle_id(&jsx->ibb_timeout_handle, g_source_remove);

	g_list_free_full(jsx->streamhosts, (GDestroyNotify)jabber_bytestreams_streamhost_free);

	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->ibb_buffer) {
		g_object_unref(jsx->ibb_buffer);
	}

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

	g_free(jsx->stream_id);
	g_free(jsx->iq_id);
	g_free(jsx->socks_buf);

	G_OBJECT_CLASS(jabber_si_xfer_parent_class)->finalize(obj);
}

static void
jabber_si_xfer_class_finalize(G_GNUC_UNUSED JabberSIXferClass *klass) {
}

static void
jabber_si_xfer_class_init(JabberSIXferClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
	PurpleXferClass *xfer_class = PURPLE_XFER_CLASS(klass);

	obj_class->finalize = jabber_si_xfer_finalize;

	xfer_class->init = jabber_si_xfer_xfer_init;
	xfer_class->request_denied = jabber_si_xfer_request_denied;
	xfer_class->cancel_send = jabber_si_xfer_cancel_send;
	xfer_class->cancel_recv = jabber_si_xfer_cancel_recv;
	xfer_class->read = jabber_si_xfer_ibb_read;
	xfer_class->write = jabber_si_xfer_ibb_write;
}

/******************************************************************************
 * Public API
 *****************************************************************************/
void
jabber_si_xfer_register(GTypeModule *module) {
	jabber_si_xfer_register_type(module);
}

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

mercurial