view libpurple/protocols/oscar/peer.c @ 27243:7054f810b0f9

Check in code that connects to oscar using clientLogin. This is the authentication scheme they've publically documented. We still use the old MD5-style login as the default, but you can optionally try this out by checking a check box on the advanced tab of your oscar account. Functionally everything is supposed to be the same. However, for some reason users with Mobile IM forwarding turned on don't show up online and can't be messaged. Not sure why. Using clientLogin DOES make it easier for AOL to track us. And yes, it probably makes it easier for AOL to block us, too. But I don't believe they want to do that. I believe they're trying to keep their network open, and I think we should appreciate that and try to work with them. We're not just some small open source project that slips under the radar unnoticed anymore. It's good to have options, right? None of this code was taken from anywhere (outside of libpurple). I wrote it all from scratch (and took a few bits from other places in libpurple). I did use the documentation on http://dev.aol.com/aim , but I don't believe that affects us from a licensing standpoint in any way. If you disagree we should talk about it on the devel mailing list.
author Mark Doliner <mark@kingant.net>
date Tue, 23 Jun 2009 18:20:12 +0000
parents 01f1929d0936
children 4e1eef14b095
line wrap: on
line source

/*
 * Purple's oscar protocol plugin
 * This file is the legal property of its developers.
 * Please see the AUTHORS file distributed alongside this file.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
*/

/*
 * Functions dealing with peer connections.  This includes the code
 * used to establish a peer connection for both Oscar File transfer
 * (OFT) and Oscar Direct Connect (ODC).  (ODC is also referred to
 * as DirectIM and IM Image.)
 */

#ifdef HAVE_CONFIG_H
#include  <config.h>
#endif

/* From the oscar PRPL */
#include "oscar.h"
#include "peer.h"

/* From Purple */
#include "conversation.h"
#include "ft.h"
#include "network.h"
#include "notify.h"
#include "request.h"
#include "util.h"

#ifndef _WIN32
#include <stdio.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> /* for inet_ntoa */
#include <limits.h> /* for UINT_MAX */
#endif

#ifdef _WIN32
#include "win32dep.h"
#endif

/*
 * I really want to switch all our networking code to using IPv6 only,
 * but that really isn't a good idea at all.  Evan S. of Adium says
 * OS X sets all connections as "AF_INET6/PF_INET6," even if there is
 * nothing inherently IPv6 about them.  And I feel like Linux kernel
 * 2.6.5 is doing the same thing.  So we REALLY should accept
 * connections if they're showing up as IPv6.  Old OSes (Solaris?)
 * that might not have full IPv6 support yet will fail if we try
 * to use PF_INET6 but it isn't defined.  --Mark Doliner
 */
#ifndef PF_INET6
#define PF_INET6 PF_INET
#endif

PeerConnection *
peer_connection_find_by_type(OscarData *od, const char *bn, OscarCapability type)
{
	GSList *cur;
	PeerConnection *conn;

	for (cur = od->peer_connections; cur != NULL; cur = cur->next)
	{
		conn = cur->data;
		if ((conn->type == type) && !oscar_util_name_compare(conn->bn, bn))
			return conn;
	}

	return NULL;
}

/**
 * @param cookie This must be exactly 8 characters.
 */
PeerConnection *
peer_connection_find_by_cookie(OscarData *od, const char *bn, const guchar *cookie)
{
	GSList *cur;
	PeerConnection *conn;

	for (cur = od->peer_connections; cur != NULL; cur = cur->next)
	{
		conn = cur->data;
		if (!memcmp(conn->cookie, cookie, 8) && !oscar_util_name_compare(conn->bn, bn))
			return conn;
	}

	return NULL;
}

PeerConnection *
peer_connection_new(OscarData *od, OscarCapability type, const char *bn)
{
	PeerConnection *conn;
	PurpleAccount *account;

	account = purple_connection_get_account(od->gc);

	conn = g_new0(PeerConnection, 1);
	conn->od = od;
	conn->type = type;
	conn->bn = g_strdup(bn);
	conn->buffer_outgoing = purple_circ_buffer_new(0);
	conn->listenerfd = -1;
	conn->fd = -1;
	conn->lastactivity = time(NULL);
	conn->use_proxy |= purple_account_get_bool(account, "always_use_rv_proxy", FALSE);

	if (type == OSCAR_CAPABILITY_DIRECTIM)
		memcpy(conn->magic, "ODC2", 4);
	else if (type == OSCAR_CAPABILITY_SENDFILE)
		memcpy(conn->magic, "OFT2", 4);

	od->peer_connections = g_slist_prepend(od->peer_connections, conn);

	return conn;
}

static void
peer_connection_close(PeerConnection *conn)
{
	if (conn->type == OSCAR_CAPABILITY_DIRECTIM)
		peer_odc_close(conn);
	else if (conn->type == OSCAR_CAPABILITY_SENDFILE)
		peer_oft_close(conn);

	if (conn->verified_connect_data != NULL)
	{
		purple_proxy_connect_cancel(conn->verified_connect_data);
		conn->verified_connect_data = NULL;
	}

	if (conn->client_connect_data != NULL)
	{
		purple_proxy_connect_cancel(conn->client_connect_data);
		conn->client_connect_data = NULL;
	}

	if (conn->listen_data != NULL)
	{
		purple_network_listen_cancel(conn->listen_data);
		conn->listen_data = NULL;
	}

	if (conn->connect_timeout_timer != 0)
	{
		purple_timeout_remove(conn->connect_timeout_timer);
		conn->connect_timeout_timer = 0;
	}

	if (conn->watcher_incoming != 0)
	{
		purple_input_remove(conn->watcher_incoming);
		conn->watcher_incoming = 0;
	}
	if (conn->watcher_outgoing != 0)
	{
		purple_input_remove(conn->watcher_outgoing);
		conn->watcher_outgoing = 0;
	}
	if (conn->listenerfd >= 0)
	{
		close(conn->listenerfd);
		conn->listenerfd = -1;
	}
	if (conn->fd >= 0)
	{
		close(conn->fd);
		conn->fd = -1;
	}

	g_free(conn->buffer_incoming.data);
	conn->buffer_incoming.data = NULL;
	conn->buffer_incoming.len = 0;
	conn->buffer_incoming.offset = 0;

	purple_circ_buffer_destroy(conn->buffer_outgoing);
	conn->buffer_outgoing = purple_circ_buffer_new(0);

	conn->flags &= ~PEER_CONNECTION_FLAG_IS_INCOMING;
}

static gboolean
peer_connection_destroy_cb(gpointer data)
{
	PeerConnection *conn;

	conn = data;

	purple_request_close_with_handle(conn);

	peer_connection_close(conn);

	if (conn->checksum_data != NULL)
		peer_oft_checksum_destroy(conn->checksum_data);

	if (conn->xfer != NULL)
	{
		PurpleXferStatusType status;
		conn->xfer->data = NULL;
		status = purple_xfer_get_status(conn->xfer);
		if ((status != PURPLE_XFER_STATUS_DONE) &&
			(status != PURPLE_XFER_STATUS_CANCEL_LOCAL) &&
			(status != PURPLE_XFER_STATUS_CANCEL_REMOTE))
		{
			if ((conn->disconnect_reason == OSCAR_DISCONNECT_REMOTE_CLOSED) ||
				(conn->disconnect_reason == OSCAR_DISCONNECT_REMOTE_REFUSED))
				purple_xfer_cancel_remote(conn->xfer);
			else
				purple_xfer_cancel_local(conn->xfer);
		}
		purple_xfer_unref(conn->xfer);
		conn->xfer = NULL;
	}

	g_free(conn->bn);
	g_free(conn->error_message);
	g_free(conn->proxyip);
	g_free(conn->clientip);
	g_free(conn->verifiedip);
	g_free(conn->xferdata.name);
	purple_circ_buffer_destroy(conn->buffer_outgoing);

	conn->od->peer_connections = g_slist_remove(conn->od->peer_connections, conn);

	g_free(conn);

	return FALSE;
}

void
peer_connection_destroy(PeerConnection *conn, OscarDisconnectReason reason, const gchar *error_message)
{
	if (conn->destroy_timeout != 0)
		purple_timeout_remove(conn->destroy_timeout);
	conn->disconnect_reason = reason;
	g_free(conn->error_message);
	conn->error_message = g_strdup(error_message);
	peer_connection_destroy_cb(conn);
}

void
peer_connection_schedule_destroy(PeerConnection *conn, OscarDisconnectReason reason, const gchar *error_message)
{
	if (conn->destroy_timeout != 0)
		/* Already taken care of */
		return;

	purple_debug_info("oscar", "Scheduling destruction of peer connection\n");
	conn->disconnect_reason = reason;
	g_free(conn->error_message);
	conn->error_message = g_strdup(error_message);
	conn->destroy_timeout = purple_timeout_add(0, peer_connection_destroy_cb, conn);
}

/*******************************************************************/
/* Begin code for receiving data on a peer connection                */
/*******************************************************************/

/**
 * This should be used to read ODC and OFT framing info.  It should
 * NOT be used to read the payload sent across the connection (IMs,
 * file data, etc), and it should NOT be used to read proxy negotiation
 * headers.
 *
 * Unlike flap_connection_recv_cb(), this only reads one frame at a
 * time.  This is done so that the watcher can be changed during the
 * handling of the frame.  If the watcher is changed then this
 * function will not read in any more data.  This happens when
 * reading the payload of a direct IM frame, or when we're
 * receiving a file from the remote user.  Once the data has been
 * read, the watcher will be switched back to this function to
 * continue reading the next frame.
 */
void
peer_connection_recv_cb(gpointer data, gint source, PurpleInputCondition cond)
{
	PeerConnection *conn;
	gssize read;

	conn = data;

	/* Start reading a new ODC/OFT frame */
	if (conn->buffer_incoming.data == NULL)
	{
		/* Read the first 6 bytes (magic string and frame length) */
		read = recv(conn->fd, conn->header + conn->header_received,
				6 - conn->header_received, 0);

		/* Check if the remote user closed the connection */
		if (read == 0)
		{
			peer_connection_destroy(conn, OSCAR_DISCONNECT_REMOTE_CLOSED, NULL);
			return;
		}

		/* If there was an error then close the connection */
		if (read < 0)
		{
			if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
				/* No worries */
				return;

			peer_connection_destroy(conn,
					OSCAR_DISCONNECT_LOST_CONNECTION, g_strerror(errno));
			return;
		}

		conn->lastactivity = time(NULL);

		/* If we don't even have the first 6 bytes then do nothing */
		conn->header_received += read;
		if (conn->header_received < 6)
			return;

		/* All ODC/OFT frames must start with a magic string */
		if (memcmp(conn->magic, conn->header, 4))
		{
			purple_debug_warning("oscar", "Expecting magic string to "
				"be %c%c%c%c but received magic string %c%c%c%c.  "
				"Closing connection.\n",
				conn->magic[0], conn->magic[1], conn->magic[2],
				conn->magic[3], conn->header[0], conn->header[1],
				conn->header[2], conn->header[3]);
			peer_connection_destroy(conn, OSCAR_DISCONNECT_INVALID_DATA, NULL);
			return;
		}

		/* Initialize a new temporary ByteStream for incoming data */
		conn->buffer_incoming.len = aimutil_get16(&conn->header[4]) - 6;
		conn->buffer_incoming.data = g_new(guint8, conn->buffer_incoming.len);
		conn->buffer_incoming.offset = 0;
	}

	/* Read data into the temporary buffer until it is complete */
	read = recv(conn->fd,
				&conn->buffer_incoming.data[conn->buffer_incoming.offset],
				conn->buffer_incoming.len - conn->buffer_incoming.offset,
				0);

	/* Check if the remote user closed the connection */
	if (read == 0)
	{
		peer_connection_destroy(conn, OSCAR_DISCONNECT_REMOTE_CLOSED, NULL);
		return;
	}

	if (read < 0)
	{
		if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
			/* No worries */
			return;

		peer_connection_destroy(conn,
				OSCAR_DISCONNECT_LOST_CONNECTION, g_strerror(errno));
		return;
	}

	conn->lastactivity = time(NULL);
	conn->buffer_incoming.offset += read;
	if (conn->buffer_incoming.offset < conn->buffer_incoming.len)
		/* Waiting for more data to arrive */
		return;

	/* We have a complete ODC/OFT frame!  Handle it and continue reading */
	byte_stream_rewind(&conn->buffer_incoming);
	if (conn->type == OSCAR_CAPABILITY_DIRECTIM)
	{
		peer_odc_recv_frame(conn, &conn->buffer_incoming);
	}
	else if (conn->type == OSCAR_CAPABILITY_SENDFILE)
	{
		peer_oft_recv_frame(conn, &conn->buffer_incoming);
	}

	g_free(conn->buffer_incoming.data);
	conn->buffer_incoming.data = NULL;

	conn->header_received = 0;
}

/*******************************************************************/
/* End code for receiving data on a peer connection                */
/*******************************************************************/

/*******************************************************************/
/* Begin code for sending data on a peer connection                */
/*******************************************************************/

static void
send_cb(gpointer data, gint source, PurpleInputCondition cond)
{
	PeerConnection *conn;
	gsize writelen;
	gssize wrotelen;

	conn = data;
	writelen = purple_circ_buffer_get_max_read(conn->buffer_outgoing);

	if (writelen == 0)
	{
		purple_input_remove(conn->watcher_outgoing);
		conn->watcher_outgoing = 0;
		/*
		 * The buffer is currently empty, so reset the current input
		 * and output positions to the start of the buffer.  We do
		 * this so that the next chunk of data that we put into the
		 * buffer can be read back out of the buffer in one fell swoop.
		 * Otherwise it gets fragmented and we have to read from the
		 * second half of the buffer than go back and read the rest of
		 * the chunk from the first half.
		 *
		 * We're using TCP, which is a stream based protocol, so this
		 * isn't supposed to matter.  However, experience has shown
		 * that at least the proxy file transfer code in AIM 6.1.41.2
		 * requires that the entire OFT frame arrive all at once.  If
		 * the frame is fragmented then AIM freaks out and aborts the
		 * file transfer.  Somebody should teach those guys how to
		 * write good TCP code.
		 */
		conn->buffer_outgoing->inptr = conn->buffer_outgoing->buffer;
		conn->buffer_outgoing->outptr = conn->buffer_outgoing->buffer;
		return;
	}

	wrotelen = send(conn->fd, conn->buffer_outgoing->outptr, writelen, 0);
	if (wrotelen <= 0)
	{
		if (wrotelen < 0 && ((errno == EAGAIN) || (errno == EWOULDBLOCK)))
			/* No worries */
			return;

		if (conn->ready)
		{
			purple_input_remove(conn->watcher_outgoing);
			conn->watcher_outgoing = 0;
			close(conn->fd);
			conn->fd = -1;
			peer_connection_schedule_destroy(conn,
					OSCAR_DISCONNECT_LOST_CONNECTION, NULL);
		}
		else
		{
			/*
			 * This could happen when unable to send a negotiation
			 * frame to a peer proxy server.
			 */
			peer_connection_trynext(conn);
		}
		return;
	}

	purple_circ_buffer_mark_read(conn->buffer_outgoing, wrotelen);
	conn->lastactivity = time(NULL);
}

/**
 * This should be called by OFT/ODC code to send a standard OFT or ODC
 * frame across the peer connection along with some payload data.  Or
 * maybe a file.  Anything, really.
 */
void
peer_connection_send(PeerConnection *conn, ByteStream *bs)
{
	/* Add everything to our outgoing buffer */
	purple_circ_buffer_append(conn->buffer_outgoing, bs->data, bs->len);

	/* If we haven't already started writing stuff, then start the cycle */
	if ((conn->watcher_outgoing == 0) && (conn->fd >= 0))
	{
		conn->watcher_outgoing = purple_input_add(conn->fd,
				PURPLE_INPUT_WRITE, send_cb, conn);
		send_cb(conn, conn->fd, 0);
	}
}

/*******************************************************************/
/* End code for sending data on a peer connection                  */
/*******************************************************************/

/*******************************************************************/
/* Begin code for establishing a peer connection                   */
/*******************************************************************/

void
peer_connection_finalize_connection(PeerConnection *conn)
{
	conn->watcher_incoming = purple_input_add(conn->fd,
			PURPLE_INPUT_READ, peer_connection_recv_cb, conn);

	if (conn->type == OSCAR_CAPABILITY_DIRECTIM)
	{
		/*
		 * If we are connecting to them then send our cookie so they
		 * can verify who we are.  Note: This doesn't seem to be
		 * necessary, but it also doesn't seem to hurt.
		 */
		if (!(conn->flags & PEER_CONNECTION_FLAG_IS_INCOMING))
			peer_odc_send_cookie(conn);
	}
	else if (conn->type == OSCAR_CAPABILITY_SENDFILE)
	{
		if (purple_xfer_get_type(conn->xfer) == PURPLE_XFER_SEND)
		{
			peer_oft_send_prompt(conn);
		}
	}

	/*
	 * Tell the remote user that we're connected (which may also imply
	 * that we've accepted their request).
	 */
	if (!(conn->flags & PEER_CONNECTION_FLAG_IS_INCOMING))
		aim_im_sendch2_connected(conn);
}

/**
 * We tried to make an outgoing connection to a remote user.  It
 * either connected or failed to connect.
 */
static void
peer_connection_common_established_cb(gpointer data, gint source, const gchar *error_message, gboolean verified)
{
	PeerConnection *conn;

	conn = data;

	if (verified)
		conn->verified_connect_data = NULL;
	else
		conn->client_connect_data = NULL;

	if (source < 0)
	{
		if ((conn->verified_connect_data == NULL) &&
			(conn->client_connect_data == NULL))
		{
			/* Our parallel connection attemps have both failed. */
			peer_connection_trynext(conn);
		}
		return;
	}

	purple_timeout_remove(conn->connect_timeout_timer);
	conn->connect_timeout_timer = 0;

	if (conn->client_connect_data != NULL)
	{
		purple_proxy_connect_cancel(conn->client_connect_data);
		conn->client_connect_data = NULL;
	}

	if (conn->verified_connect_data != NULL)
	{
		purple_proxy_connect_cancel(conn->verified_connect_data);
		conn->verified_connect_data = NULL;
	}

	conn->fd = source;

	peer_connection_finalize_connection(conn);
}

static void
peer_connection_verified_established_cb(gpointer data, gint source, const gchar *error_message)
{
	peer_connection_common_established_cb(data, source, error_message, TRUE);
}

static void
peer_connection_client_established_cb(gpointer data, gint source, const gchar *error_message)
{
	peer_connection_common_established_cb(data, source, error_message, FALSE);
}

/**
 * This is the watcher callback for any listening socket that is
 * waiting for a peer to connect.  When a peer connects we set the
 * input watcher to start reading data from the peer.
 *
 * To make sure that the connection is with the intended person and
 * not with a malicious middle man, we don't send anything until we've
 * received a peer frame from the remote user and have verified that
 * the cookie in the peer frame matches the cookie that was exchanged
 * in the channel 2 ICBM.
 */
void
peer_connection_listen_cb(gpointer data, gint source, PurpleInputCondition cond)
{
	PeerConnection *conn;
	OscarData *od;
	PurpleConnection *gc;
	struct sockaddr addr;
	socklen_t addrlen = sizeof(addr);
	int flags;

	conn = data;
	od = conn->od;
	gc = od->gc;

	purple_debug_info("oscar", "Accepting connection on listener socket.\n");

	conn->fd = accept(conn->listenerfd, &addr, &addrlen);
	if (conn->fd < 0)
	{
		if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
			/* No connection yet--no worries */
			/* TODO: Hmm, but they SHOULD be connected if we're here, right? */
			return;

		peer_connection_trynext(conn);
		return;
	}

	if ((addr.sa_family != PF_INET) && (addr.sa_family != PF_INET6))
	{
		/* Invalid connection type?!  Continue waiting. */
		close(conn->fd);
		return;
	}

	flags = fcntl(conn->fd, F_GETFL);
	fcntl(conn->fd, F_SETFL, flags | O_NONBLOCK);
#ifndef _WIN32
	fcntl(conn->fd, F_SETFD, FD_CLOEXEC);
#endif

	purple_input_remove(conn->watcher_incoming);

	peer_connection_finalize_connection(conn);
}

/**
 * We've just opened a listener socket, so we send the remote
 * user an ICBM and ask them to connect to us.
 */
static void
peer_connection_establish_listener_cb(int listenerfd, gpointer data)
{
	PeerConnection *conn;
	OscarData *od;
	PurpleConnection *gc;
	PurpleAccount *account;
	PurpleConversation *conv;
	char *tmp;
	FlapConnection *bos_conn;
	const char *listener_ip;
	unsigned short listener_port;

	conn = data;
	conn->listen_data = NULL;

	if (listenerfd < 0)
	{
		/* Could not open listener socket */
		peer_connection_trynext(conn);
		return;
	}

	od = conn->od;
	gc = od->gc;
	account = purple_connection_get_account(gc);
	conn->listenerfd = listenerfd;

	/* Watch for new connections on our listener socket */
	conn->watcher_incoming = purple_input_add(conn->listenerfd,
			PURPLE_INPUT_READ, peer_connection_listen_cb, conn);

	/* Send the "please connect to me!" ICBM */
	bos_conn = flap_connection_findbygroup(od, SNAC_FAMILY_ICBM);
	if (bos_conn == NULL)
	{
		/* Not good */
		peer_connection_trynext(conn);
		return;
	}

	if (bos_conn->gsc)
		listener_ip = purple_network_get_my_ip(bos_conn->gsc->fd);
	else
		listener_ip = purple_network_get_my_ip(bos_conn->fd);
	listener_port = purple_network_get_port_from_fd(conn->listenerfd);
	if (conn->type == OSCAR_CAPABILITY_DIRECTIM)
	{
		aim_im_sendch2_odc_requestdirect(od,
				conn->cookie, conn->bn, purple_network_ip_atoi(listener_ip),
				listener_port, ++conn->lastrequestnumber);

		/* Print a message to a local conversation window */
		conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, conn->bn);
		tmp = g_strdup_printf(_("Asking %s to connect to us at %s:%hu for "
				"Direct IM."), conn->bn, listener_ip, listener_port);
		purple_conversation_write(conv, NULL, tmp, PURPLE_MESSAGE_SYSTEM, time(NULL));
		g_free(tmp);
	}
	else if (conn->type == OSCAR_CAPABILITY_SENDFILE)
	{
		aim_im_sendch2_sendfile_requestdirect(od,
				conn->cookie, conn->bn,
				purple_network_ip_atoi(listener_ip),
				listener_port, ++conn->lastrequestnumber,
				(const gchar *)conn->xferdata.name,
				conn->xferdata.size, conn->xferdata.totfiles);
	}
}

/**
 * This is a callback function used when we're connecting to a peer
 * using either the client IP or the verified IP and the connection
 * took longer than 5 seconds to complete.  We do this because
 * waiting for the OS to time out the connection attempt is not
 * practical--the default timeout on many OSes can be 3 minutes or
 * more, and users are impatient.
 *
 * Worst case scenario: the user is connected to the Internet using
 * a modem with severe lag.  The peer connections fail and Purple falls
 * back to using a proxied connection.  The lower bandwidth
 * limitations imposed by the proxied connection won't matter because
 * the user is using a modem.
 *
 * I suppose this line of thinking is discriminatory against people
 * with very high lag but decent throughput who are transferring
 * large files.  But we don't care about those people.
 *
 * I (Sean) changed the timeout from 15 to 5 seconds, as 60 seconds is
 * too long for a user to wait to send a file. I'm also parallelizing
 * requests when possible. The longest we should have to wait now is 10
 * seconds. We shouldn't make it shorter than this.
 */
static gboolean
peer_connection_tooktoolong(gpointer data)
{
	PeerConnection *conn;

	conn = data;

	purple_debug_info("oscar", "Peer connection timed out after 5 seconds.  "
			"Trying next method...\n");

	peer_connection_trynext(conn);

	/* Cancel this timer.  It'll be added again, if needed. */
	return FALSE;
}

/**
 * Try to establish the given PeerConnection using a defined
 * sequence of steps.
 */
void
peer_connection_trynext(PeerConnection *conn)
{
	PurpleAccount *account;

	account = purple_connection_get_account(conn->od->gc);

	/*
	 * Close any remnants of a previous failed connection attempt.
	 */
	peer_connection_close(conn);

	/*
	 * 1. Attempt to connect to the remote user using their verifiedip and clientip.
	 *    We try these at the same time and use whichever succeeds first, so we don't
	 *    have to wait for a timeout.
	 */
	if (!(conn->flags & PEER_CONNECTION_FLAG_TRIED_DIRECT) &&
		(conn->verifiedip != NULL) && (conn->port != 0) && (!conn->use_proxy))
	{
		conn->flags |= PEER_CONNECTION_FLAG_TRIED_DIRECT;

		if (conn->type == OSCAR_CAPABILITY_DIRECTIM)
		{
			gchar *tmp;
			PurpleConversation *conv;
			tmp = g_strdup_printf(_("Attempting to connect to %s:%hu."),
					conn->verifiedip, conn->port);
			conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, conn->bn);
			purple_conversation_write(conv, NULL, tmp,
					PURPLE_MESSAGE_SYSTEM, time(NULL));
			g_free(tmp);
		}

		conn->verified_connect_data = purple_proxy_connect(NULL, account,
				conn->verifiedip, conn->port,
				peer_connection_verified_established_cb, conn);

		if ((conn->verifiedip == NULL) ||
			strcmp(conn->verifiedip, conn->clientip))
		{
			conn->client_connect_data = purple_proxy_connect(NULL, account,
					conn->clientip, conn->port,
					peer_connection_client_established_cb, conn);
		}

		if ((conn->verified_connect_data != NULL) ||
			(conn->client_connect_data != NULL))
		{
			/* Connecting... */
			conn->connect_timeout_timer = purple_timeout_add_seconds(5,
					peer_connection_tooktoolong, conn);
			return;
		}
	}

	/*
	 * 2. Attempt to have the remote user connect to us (using both
	 *    our verifiedip and our clientip).
	 */
	if (!(conn->flags & PEER_CONNECTION_FLAG_TRIED_INCOMING) &&
		(!conn->use_proxy))
	{
		conn->flags |= PEER_CONNECTION_FLAG_TRIED_INCOMING;

		/*
		 * Remote user is connecting to us, so we'll need to verify
		 * that the user who connected is our friend.
		 */
		conn->flags |= PEER_CONNECTION_FLAG_IS_INCOMING;

		conn->listen_data = purple_network_listen_range(5190, 5290, SOCK_STREAM,
				peer_connection_establish_listener_cb, conn);
		if (conn->listen_data != NULL)
		{
			/* Opening listener socket... */
			return;
		}
	}

	/*
	 * 3. Attempt to have both users connect to an intermediate proxy
	 *    server.
	 */
	if (!(conn->flags & PEER_CONNECTION_FLAG_TRIED_PROXY))
	{
		conn->flags |= PEER_CONNECTION_FLAG_TRIED_PROXY;

		/*
		 * If we initiate the proxy connection, then the remote user
		 * could be anyone, so we need to verify that the user who
		 * connected is our friend.
		 */
		if (!conn->use_proxy)
			conn->flags |= PEER_CONNECTION_FLAG_IS_INCOMING;

		if (conn->type == OSCAR_CAPABILITY_DIRECTIM)
		{
			gchar *tmp;
			PurpleConversation *conv;
			tmp = g_strdup(_("Attempting to connect via proxy server."));
			conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, conn->bn);
			purple_conversation_write(conv, NULL, tmp,
					PURPLE_MESSAGE_SYSTEM, time(NULL));
			g_free(tmp);
		}

		conn->verified_connect_data = purple_proxy_connect(NULL, account,
				(conn->proxyip != NULL) ? conn->proxyip : PEER_PROXY_SERVER,
				PEER_PROXY_PORT,
				peer_proxy_connection_established_cb, conn);
		if (conn->verified_connect_data != NULL)
		{
			/* Connecting... */
			return;
		}
	}

	/* Give up! */
	peer_connection_destroy(conn, OSCAR_DISCONNECT_COULD_NOT_CONNECT, NULL);
}

/**
 * Initiate a peer connection with someone.
 */
void
peer_connection_propose(OscarData *od, OscarCapability type, const char *bn)
{
	PeerConnection *conn;

	if (type == OSCAR_CAPABILITY_DIRECTIM)
	{
		conn = peer_connection_find_by_type(od, bn, type);
		if (conn != NULL)
		{
			if (conn->ready)
			{
				PurpleAccount *account;
				PurpleConversation *conv;

				purple_debug_info("oscar", "Already have a direct IM "
						"session with %s.\n", bn);
				account = purple_connection_get_account(od->gc);
				conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM,
						bn, account);
				if (conv != NULL)
					purple_conversation_present(conv);
				return;
			}

			/* Cancel the old connection and try again */
			peer_connection_destroy(conn, OSCAR_DISCONNECT_RETRYING, NULL);
		}
	}

	conn = peer_connection_new(od, type, bn);
	conn->flags |= PEER_CONNECTION_FLAG_INITIATED_BY_ME;
	conn->flags |= PEER_CONNECTION_FLAG_APPROVED;
	aim_icbm_makecookie(conn->cookie);

	peer_connection_trynext(conn);
}

/**
 * Someone else wants to establish a peer connection with us,
 * and we said yes.
 */
static void
peer_connection_got_proposition_yes_cb(gpointer data, gint id)
{
	PeerConnection *conn;

	conn = data;

	conn->flags |= PEER_CONNECTION_FLAG_APPROVED;
	peer_connection_trynext(conn);
}

/**
 * Someone else wants to establish a peer connection with us,
 * and we said no.
 *
 * "Well, one time my friend asked me if I wanted to play the
 *  piccolo.  But I said no."
 */
static void
peer_connection_got_proposition_no_cb(gpointer data, gint id)
{
	PeerConnection *conn;

	conn = data;

	aim_im_denytransfer(conn->od, conn->bn, conn->cookie,
			AIM_TRANSFER_DENY_DECLINE);
	peer_connection_destroy(conn, OSCAR_DISCONNECT_LOCAL_CLOSED, NULL);
}

/**
 * Someone else wants to establish a peer connection with us.
 */
void
peer_connection_got_proposition(OscarData *od, const gchar *bn, const gchar *message, IcbmArgsCh2 *args)
{
	PurpleConnection *gc;
	PurpleAccount *account;
	PeerConnection *conn;
	gchar *buf;

	gc = od->gc;
	account = purple_connection_get_account(gc);

	/*
	 * If we have a connection with this same cookie then they are
	 * probably just telling us they weren't able to connect to us
	 * and we should try connecting to them, instead.  Or they want
	 * to go through a proxy.
	 */
	conn = peer_connection_find_by_cookie(od, bn, args->cookie);
	if ((conn != NULL) && (conn->type == args->type))
	{
		purple_debug_info("oscar", "Remote user wants to try a "
				"different connection method\n");
		g_free(conn->proxyip);
		g_free(conn->clientip);
		g_free(conn->verifiedip);
		if (args->use_proxy)
			conn->proxyip = g_strdup(args->proxyip);
		else
			conn->proxyip = NULL;
		conn->verifiedip = g_strdup(args->verifiedip);
		conn->clientip = g_strdup(args->clientip);
		conn->port = args->port;
		conn->use_proxy |= args->use_proxy;
		conn->lastrequestnumber++;
		peer_connection_trynext(conn);
		return;
	}

	/* If this is a direct IM, then close any existing session */
	if (args->type == OSCAR_CAPABILITY_DIRECTIM)
	{
		conn = peer_connection_find_by_type(od, bn, args->type);
		if (conn != NULL)
		{
			/* Close the old direct IM and start a new one */
			purple_debug_info("oscar", "Received new direct IM request "
				"from %s.  Destroying old connection.\n", bn);
			peer_connection_destroy(conn, OSCAR_DISCONNECT_REMOTE_CLOSED, NULL);
		}
	}

	/* Check for proper arguments */
	if (args->type == OSCAR_CAPABILITY_SENDFILE)
	{
		if ((args->info.sendfile.filename == NULL) ||
			(args->info.sendfile.totsize == 0) ||
			(args->info.sendfile.totfiles == 0))
		{
			purple_debug_warning("oscar",
					"%s tried to send you a file with incomplete "
					"information.\n", bn);
			return;
		}
	}

	conn = peer_connection_new(od, args->type, bn);
	memcpy(conn->cookie, args->cookie, 8);
	if (args->use_proxy)
		conn->proxyip = g_strdup(args->proxyip);
	conn->clientip = g_strdup(args->clientip);
	conn->verifiedip = g_strdup(args->verifiedip);
	conn->port = args->port;
	conn->use_proxy |= args->use_proxy;
	conn->lastrequestnumber++;

	if (args->type == OSCAR_CAPABILITY_DIRECTIM)
	{
		buf = g_strdup_printf(_("%s has just asked to directly connect to %s"),
				bn, purple_account_get_username(account));

		purple_request_action(conn, NULL, buf,
						_("This requires a direct connection between "
						  "the two computers and is necessary for IM "
						  "Images.  Because your IP address will be "
						  "revealed, this may be considered a privacy "
						  "risk."),
						PURPLE_DEFAULT_ACTION_NONE,
						account, bn, NULL,
						conn, 2,
						_("C_onnect"), G_CALLBACK(peer_connection_got_proposition_yes_cb),
						_("Cancel"), G_CALLBACK(peer_connection_got_proposition_no_cb));
	}
	else if (args->type == OSCAR_CAPABILITY_SENDFILE)
	{
		gchar *filename;

		conn->xfer = purple_xfer_new(account, PURPLE_XFER_RECEIVE, bn);
		if (conn->xfer)
		{
			conn->xfer->data = conn;
			purple_xfer_ref(conn->xfer);
			purple_xfer_set_size(conn->xfer, args->info.sendfile.totsize);

			/* Set the file name */
			if (g_utf8_validate(args->info.sendfile.filename, -1, NULL))
				filename = g_strdup(args->info.sendfile.filename);
			else
				filename = purple_utf8_salvage(args->info.sendfile.filename);

			if (args->info.sendfile.subtype == AIM_OFT_SUBTYPE_SEND_DIR)
			{
				/*
				 * If they are sending us a directory then the last character
				 * of the file name will be an asterisk.  We don't want to
				 * save stuff to a directory named "*" so we remove the
				 * asterisk from the file name.
				 */
				char *tmp = strrchr(filename, '\\');
				if ((tmp != NULL) && (tmp[1] == '*'))
					tmp[0] = '\0';
			}
			purple_xfer_set_filename(conn->xfer, filename);
			g_free(filename);

			/*
			 * Set the message, unless this is the dummy message from an
			 * ICQ client or an empty message from an AIM client.
			 * TODO: Maybe we should strip HTML and then see if strlen>0?
			 */
			if ((message != NULL) &&
				(g_ascii_strncasecmp(message, "<ICQ_COOL_FT>", 13) != 0) &&
				(g_ascii_strcasecmp(message, "<HTML>") != 0))
			{
				purple_xfer_set_message(conn->xfer, message);
			}

			/* Setup our I/O op functions */
			purple_xfer_set_init_fnc(conn->xfer, peer_oft_recvcb_init);
			purple_xfer_set_end_fnc(conn->xfer, peer_oft_recvcb_end);
			purple_xfer_set_request_denied_fnc(conn->xfer, peer_oft_cb_generic_cancel);
			purple_xfer_set_cancel_recv_fnc(conn->xfer, peer_oft_cb_generic_cancel);
			purple_xfer_set_ack_fnc(conn->xfer, peer_oft_recvcb_ack_recv);

			/* Now perform the request */
			purple_xfer_request(conn->xfer);
		}
	}
}

/*******************************************************************/
/* End code for establishing a peer connection                     */
/*******************************************************************/