view libpurple/protocols/msn/directconn.c @ 30044:87854e2529a0

It seems like every DC packet requires a length, so why make every little function set that up?
author Elliott Sales de Andrade <qulogic@pidgin.im>
date Sun, 25 Apr 2010 23:38:27 +0000
parents 97942038af05
children 63684aa5d6ab
line wrap: on
line source

/**
 * @file directconn.c MSN direct connection functions
 *
 * purple
 *
 * 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 "msn.h"
#include "directconn.h"

#include "slp.h"
#include "slpmsg.h"

#pragma pack(push,1)
typedef struct {
	guint32 session_id;
	guint32 seq_id;
	guint64 offset;
	guint64 total_size;
	guint32 length;
	guint32 flags;
	guint32 ack_id;
	guint32 ack_uid;
	guint64 ack_size;
/*	guint8  body[1]; */
} MsnDcContext;
#pragma pack(pop)

#define DC_PACKET_HEADER_SIZE sizeof(MsnDcContext)
#define DC_MAX_BODY_SIZE      1352
#define DC_MAX_PACKET_SIZE    (DC_PACKET_HEADER_SIZE + DC_MAX_BODY_SIZE)

static void
msn_dc_calculate_nonce_hash(MsnDirectConnNonceType type,
                            const guchar nonce[16], gchar nonce_hash[37])
{
	guchar digest[20];

	if (type == DC_NONCE_SHA1) {
		PurpleCipher *cipher = purple_ciphers_find_cipher("sha1");
		PurpleCipherContext *context = purple_cipher_context_new(cipher, NULL);
		purple_cipher_context_append(context, nonce, sizeof(nonce));
		purple_cipher_context_digest(context, sizeof(digest), digest, NULL);
		purple_cipher_context_destroy(context);
	} else if (type == DC_NONCE_PLAIN) {
		memcpy(digest, nonce, 16);
	}

	g_sprintf(nonce_hash,
	          "%08X-%04X-%04X-%04X-%08X%04X",
	          GUINT32_FROM_LE(*((guint32 *)(digest + 0))),
	          GUINT16_FROM_LE(*((guint16 *)(digest + 4))),
	          GUINT16_FROM_LE(*((guint16 *)(digest + 6))),
	          GUINT16_FROM_BE(*((guint16 *)(digest + 8))),
	          GUINT32_FROM_BE(*((guint32 *)(digest + 10))),
	          GUINT16_FROM_BE(*((guint16 *)(digest + 14)))
	);
}

static void
msn_dc_generate_nonce(MsnDirectConn *dc)
{
	guint32 *nonce;
	int i;

	nonce = (guint32 *)&dc->nonce;
	for (i = 0; i < 4; i++)
		nonce[i] = rand();

	msn_dc_calculate_nonce_hash(dc->nonce_type, dc->nonce, dc->nonce_hash);

	if (purple_debug_is_verbose())
		purple_debug_info("msn", "DC %p generated nonce %s\n", dc, dc->nonce_hash);
}

static MsnDirectConnPacket *
msn_dc_new_packet(guint32 length)
{
	MsnDirectConnPacket	*p;

	p = g_new0(MsnDirectConnPacket, 1);
	p->length = length + 4;
	p->data = g_malloc(length);

	return p;
}

static void
msn_dc_destroy_packet(MsnDirectConnPacket *p)
{
	g_free(p->data);

	if (p->msg)
		msn_message_unref(p->msg);

	g_free(p);
}

MsnDirectConn *
msn_dc_new(MsnSlpCall *slpcall)
{
	MsnDirectConn *dc;

	g_return_val_if_fail(slpcall != NULL, NULL);

	dc = g_new0(MsnDirectConn, 1);

	if (purple_debug_is_verbose())
		purple_debug_info("msn", "msn_dc_new %p\n", dc);

	dc->slplink = slpcall->slplink;
	dc->slpcall = slpcall;

	if (dc->slplink->dc != NULL)
		purple_debug_warning("msn", "msn_dc_new: slplink already has an allocated DC!\n");

	dc->slplink->dc = dc;

	dc->msg_body = NULL;
	dc->prev_ack = NULL;
	dc->listen_data = NULL;
	dc->connect_data = NULL;
	dc->listenfd = -1;
	dc->listenfd_handle = 0;
	dc->connect_timeout_handle = 0;
	dc->fd = -1;
	dc->recv_handle = 0;
	dc->send_handle = 0;
	dc->state = DC_STATE_CLOSED;
	dc->in_buffer = NULL;
	dc->out_queue = g_queue_new();
	dc->msg_pos = -1;
	dc->send_connection_info_msg_cb = NULL;
	dc->ext_ip = NULL;
	dc->timeout_handle = 0;
	dc->progress = FALSE;
	//dc->num_calls = 1;

	/* TODO: Probably should set this based on buddy caps */
	dc->nonce_type = DC_NONCE_PLAIN;
	msn_dc_generate_nonce(dc);

	return dc;
}

void
msn_dc_destroy(MsnDirectConn *dc)
{
	MsnSlpLink *slplink;

	if (purple_debug_is_verbose())
		purple_debug_info("msn", "msn_dc_destroy %p\n", dc);

	g_return_if_fail(dc != NULL);

	slplink = dc->slplink;

	if (dc->slpcall != NULL)
		dc->slpcall->wait_for_socket = FALSE;

	slplink->dc = NULL;

	if (slplink->swboard == NULL)
		msn_slplink_destroy(slplink);

	g_free(dc->msg_body);

	if (dc->prev_ack) {
		msn_slpmsg_destroy(dc->prev_ack);
	}

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

	if (dc->connect_data != NULL) {
		purple_proxy_connect_cancel(dc->connect_data);
	}

	if (dc->listenfd != -1) {
		purple_network_remove_port_mapping(dc->listenfd);
		close(dc->listenfd);
	}

	if (dc->listenfd_handle != 0) {
		purple_timeout_remove(dc->listenfd_handle);
	}

	if (dc->connect_timeout_handle != 0) {
		purple_timeout_remove(dc->connect_timeout_handle);
	}

	if (dc->fd != -1) {
		close(dc->fd);
	}

	if (dc->send_handle != 0) {
		purple_input_remove(dc->send_handle);
	}

	if (dc->recv_handle != 0) {
		purple_input_remove(dc->recv_handle);
	}

	g_free(dc->in_buffer);

	if (dc->out_queue != NULL) {
		while (!g_queue_is_empty(dc->out_queue))
			msn_dc_destroy_packet( g_queue_pop_head(dc->out_queue) );

		g_queue_free(dc->out_queue);
	}

	g_free(dc->ext_ip);

	if (dc->timeout_handle != 0) {
		purple_timeout_remove(dc->timeout_handle);
	}

	g_free(dc);
}

/*
void
msn_dc_ref(MsnDirectConn *dc)
{
	g_return_if_fail(dc != NULL);

	dc->num_calls++;
}

void
msn_dc_unref(MsnDirectConn *dc)
{
	g_return_if_fail(dc != NULL);


	if (dc->num_calls > 0) {
		dc->num_calls--;
	}
}
*/

void
msn_dc_send_invite(MsnDirectConn *dc)
{
	MsnSlpCall    *slpcall;
	MsnSlpMessage *msg;
	gchar *header;

	if (purple_debug_is_verbose())
		purple_debug_info("msn", "msn_dc_send_invite %p\n", dc);

	g_return_if_fail(dc != NULL);

	slpcall = dc->slpcall;
	g_return_if_fail(slpcall != NULL);

	header = g_strdup_printf(
		"INVITE MSNMSGR:%s MSNSLP/1.0",
		slpcall->slplink->remote_user
	);

	msg = msn_slpmsg_sip_new(
		slpcall,
		0,
		header,
		slpcall->branch,
		"application/x-msnmsgr-transrespbody",
		dc->msg_body
	);
	msg->info = "DC INVITE";
	msg->text_body = TRUE;
	g_free(header);
	g_free(dc->msg_body);
	dc->msg_body = NULL;

	msn_slplink_queue_slpmsg(slpcall->slplink, msg);
}

void
msn_dc_send_ok(MsnDirectConn *dc)
{
	if (purple_debug_is_verbose())
		purple_debug_info("msn", "msn_dc_send_ok %p\n", dc);

	g_return_if_fail(dc != NULL);

	msn_slp_send_ok(dc->slpcall, dc->slpcall->branch,
		"application/x-msnmsgr-transrespbody", dc->msg_body);
	g_free(dc->msg_body);
	dc->msg_body = NULL;

	msn_slplink_send_slpmsg(dc->slpcall->slplink, dc->prev_ack);
	msn_slpmsg_destroy(dc->prev_ack);
	dc->prev_ack = NULL;
	msn_slplink_send_queued_slpmsgs(dc->slpcall->slplink);
}

static void
msn_dc_fallback_to_p2p(MsnDirectConn *dc)
{
	MsnSlpCall *slpcall;
	PurpleXfer *xfer;

	purple_debug_info("msn", "msn_dc_try_fallback_to_p2p %p\n", dc);

	g_return_if_fail(dc != NULL);

	slpcall = dc->slpcall;
	g_return_if_fail(slpcall != NULL);

	xfer = slpcall->xfer;
	g_return_if_fail(xfer != NULL);

	msn_dc_destroy(dc);

	msn_slpcall_session_init(slpcall);

	/*
	switch (purple_xfer_get_status(xfer)) {
	case PURPLE_XFER_STATUS_NOT_STARTED:
	case PURPLE_XFER_STATUS_ACCEPTED:
		msn_slpcall_session_init(slpcall);
		break;

	case PURPLE_XFER_STATUS_STARTED:
		slpcall->session_init_cb = NULL;
		slpcall->end_cb = NULL;
		slpcall->progress_cb = NULL;
		slpcall->cb = NULL;

		if (fail_local)
			purple_xfer_cancel_local(xfer);
		else
			purple_xfer_cancel_remote(xfer);
		break;

	default:
		slpcall->session_init_cb = NULL;
		slpcall->end_cb = NULL;
		slpcall->progress_cb = NULL;
		slpcall->cb = NULL;

		if (fail_local)
			purple_xfer_cancel_local(xfer);
		else
			purple_xfer_cancel_remote(xfer);

		break;
	}
	*/
}

static void
msn_dc_parse_binary_header(MsnDirectConn *dc)
{
	MsnSlpHeader *h;
	MsnDcContext *context;

	g_return_if_fail(dc != NULL);

	h = &dc->header;
	/* Skip packet size */
	context = (MsnDcContext *)(dc->in_buffer + 4);

	h->session_id = GUINT32_FROM_LE(context->session_id);
	h->id = GUINT32_FROM_LE(context->seq_id);
	h->offset = GUINT64_FROM_LE(context->offset);
	h->total_size = GUINT64_FROM_LE(context->total_size);
	h->length = GUINT32_FROM_LE(context->length);
	h->flags = GUINT32_FROM_LE(context->flags);
	h->ack_id = GUINT32_FROM_LE(context->ack_id);
	h->ack_sub_id = GUINT32_FROM_LE(context->ack_uid);
	h->ack_size = GUINT64_FROM_LE(context->ack_size);
}

static const gchar *
msn_dc_serialize_binary_header(MsnDirectConn *dc) {
	MsnSlpHeader *h;
	static MsnDcContext bin_header;

	g_return_val_if_fail(dc != NULL, NULL);

	h = &dc->header;

	bin_header.session_id = GUINT32_TO_LE(h->session_id);
	bin_header.seq_id = GUINT32_TO_LE(h->id);
	bin_header.offset = GUINT64_TO_LE(h->offset);
	bin_header.total_size = GUINT64_TO_LE(h->total_size);
	bin_header.length = GUINT32_TO_LE(h->length);
	bin_header.flags = GUINT32_TO_LE(h->flags);
	bin_header.ack_id = GUINT32_TO_LE(h->ack_id);
	bin_header.ack_uid = GUINT32_TO_LE(h->ack_sub_id);
	bin_header.ack_size = GUINT64_TO_LE(h->ack_size);

	return (const gchar *)&bin_header;
}

/*
static void
msn_dc_send_bye(MsnDirectConn *dc)
{
	MsnSlpLink *slplink;
	PurpleAccount *account;
	char *body;
	int body_len;

	if (purple_debug_is_verbose())
		purple_debug_info("msn", "msn_dc_send_bye %p\n", dc);

	g_return_if_fail(dc != NULL);
	g_return_if_fail(dc->slpcall != NULL);

	slplink = dc->slpcall->slplink;
	account = slplink->session->account;

	dc->header.session_id = 0;
	dc->header.id = dc->slpcall->slplink->slp_seq_id++;
	dc->header.offset = 0;

	body = g_strdup_printf(
		"BYE MSNMSGR:%s MSNSLP/1.0\r\n"
		"To: <msnmsgr:%s>\r\n"
		"From: <msnmsgr:%s>\r\n"
		"Via: MSNSLP/1.0/TLP ;branch={%s}\r\n"
		"CSeq: 0\r\n"
		"Call-ID: {%s}\r\n"
		"Max-Forwards: 0\r\n"
		"Content-Type: application/x-msnmsgr-sessionclosebody\r\n"
		"Content-Length: 3\r\n"
		"\r\n\r\n",

		slplink->remote_user,
		slplink->remote_user,
		purple_account_get_username(account),
		dc->slpcall->branch,
		dc->slpcall->id
	);
	body_len = strlen(body) + 1;
	memcpy(dc->buffer, body, body_len);
	g_free(body);

	dc->header.total_size = body_len;
	dc->header.length = body_len;
	dc->header.flags = 0;
	dc->header.ack_sub_id = 0;
	dc->header.ack_size = 0;

	msn_dc_send_packet(dc);
}

static void
msn_dc_send_ack(MsnDirectConn *dc)
{
	g_return_if_fail(dc != NULL);

	dc->header.session_id = 0;
	dc->header.ack_sub_id = dc->header.ack_id;
	dc->header.ack_id = dc->header.id;
	dc->header.id = dc->slpcall->slplink->slp_seq_id++;
	dc->header.offset = 0;
	dc->header.length = 0;
	dc->header.flags = 0x02;
	dc->header.ack_size = dc->header.total_size;

	msn_dc_send_packet(dc);
}

static void
msn_dc_send_data_ack(MsnDirectConn *dc)
{
	g_return_if_fail(dc != NULL);

	dc->header.session_id = dc->slpcall->session_id;
	dc->header.ack_sub_id = dc->header.ack_id;
	dc->header.ack_id = dc->header.id;
	dc->header.id = dc->slpcall->slplink->slp_seq_id++;
	dc->header.offset = 0;
	dc->header.length = 0;
	dc->header.flags = 0x02;
	dc->header.ack_size = dc->header.total_size;

	msn_dc_send_packet(dc);
}

static void
msn_dc_xfer_send_cancel(PurpleXfer *xfer)
{
	MsnSlpCall *slpcall;
	MsnDirectConn *dc;

	purple_debug_info("msn", "msn_dc_xfer_send_cancel\n");

	g_return_if_fail(xfer != NULL);

	slpcall = xfer->data;
	g_return_if_fail(slpcall != NULL);

	dc = slpcall->dc;
	g_return_if_fail(dc != NULL);

	switch (dc->state) {
	case DC_STATE_TRANSFER:
		msn_dc_send_bye(dc);
		dc->state = DC_STATE_CANCELLED;
		break;

	default:
		msn_dc_destroy(dc);
		break;
	}
}

static void
msn_dc_xfer_recv_cancel(PurpleXfer *xfer)
{
	MsnSlpCall *slpcall;
	MsnDirectConn *dc;

	purple_debug_info("msn", "msn_dc_xfer_recv_cancel\n");

	g_return_if_fail(xfer != NULL);

	slpcall = xfer->data;
	g_return_if_fail(slpcall != NULL);

	dc = slpcall->dc;
	g_return_if_fail(dc != NULL);

	switch (dc->state) {
	case DC_STATE_TRANSFER:
		msn_dc_send_bye(dc);
		dc->state = DC_STATE_CANCELLED;
		break;

	default:
		msn_dc_destroy(dc);
		break;
	}
}
*/

static void
msn_dc_send_cb(gpointer data, gint fd, PurpleInputCondition cond)
{
	MsnDirectConn *dc = data;
	MsnDirectConnPacket *p;
	int bytes_to_send;
	int bytes_sent;

	g_return_if_fail(dc != NULL);
	g_return_if_fail(fd != -1);

	if (g_queue_is_empty(dc->out_queue)) {
		if (dc->send_handle != 0) {
			purple_input_remove(dc->send_handle);
			dc->send_handle = 0;
		}
		return;
	}

	p = g_queue_peek_head(dc->out_queue);

	if (dc->msg_pos < 0) {
		/* First we send the length of the packet */
		guint32 len = GUINT32_TO_LE(p->length);
		bytes_sent = send(fd, &len, 4, 0);
		if (bytes_sent < 0) {
			if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
				return;

			purple_debug_warning("msn", "msn_dc_send_cb: send error\n");
			msn_dc_destroy(dc);
			return;
		}
		dc->msg_pos = 0;
	}

	bytes_to_send = p->length - dc->msg_pos;
	bytes_sent = send(fd, p->data, bytes_to_send, 0);
	if (bytes_sent < 0) {
		if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
			return;

		purple_debug_warning("msn", "msn_dc_send_cb: send error\n");
		msn_dc_destroy(dc);
		return;
	}

	dc->progress = TRUE;

	dc->msg_pos += bytes_sent;
	if (dc->msg_pos == p->length) {
		if (p->sent_cb != NULL)
			p->sent_cb(p);

		g_queue_pop_head(dc->out_queue);
		msn_dc_destroy_packet(p);

		dc->msg_pos = -1;
	}
}

static void
msn_dc_enqueue_packet(MsnDirectConn *dc, MsnDirectConnPacket *p)
{
	gboolean was_empty;

	was_empty = g_queue_is_empty(dc->out_queue);
	g_queue_push_tail(dc->out_queue, p);

	if (was_empty && dc->send_handle == 0) {
		dc->send_handle = purple_input_add(dc->fd, PURPLE_INPUT_WRITE, msn_dc_send_cb, dc);
		msn_dc_send_cb(dc, dc->fd, PURPLE_INPUT_WRITE);
	}
}

static void
msn_dc_send_foo(MsnDirectConn *dc)
{
	MsnDirectConnPacket	*p;

	if (purple_debug_is_verbose())
		purple_debug_info("msn", "msn_dc_send_foo %p\n", dc);

	p = msn_dc_new_packet(4);

	memcpy(p->data, "foo\0", 4);

	msn_dc_enqueue_packet(dc, p);
}

static void
msn_dc_send_handshake(MsnDirectConn *dc)
{
	MsnDirectConnPacket *p;
	const gchar *h;

	p = msn_dc_new_packet(DC_PACKET_HEADER_SIZE);

	dc->header.session_id = 0;
	dc->header.id = dc->slpcall->slplink->slp_seq_id++;
	dc->header.offset = 0;
	dc->header.total_size = 0;
	dc->header.length = 0;
	dc->header.flags = 0x100;

	h = msn_dc_serialize_binary_header(dc);
	memcpy(p->data, h, DC_PACKET_HEADER_SIZE);
	memcpy(p->data + offsetof(MsnDcContext, ack_id), dc->nonce, 16);

	msn_dc_enqueue_packet(dc, p);
}

static void
msn_dc_send_handshake_reply(MsnDirectConn *dc)
{
	MsnDirectConnPacket *p;
	const gchar *h;

	p = msn_dc_new_packet(DC_PACKET_HEADER_SIZE);

	dc->header.id = dc->slpcall->slplink->slp_seq_id++;
	dc->header.length = 0;

	h = msn_dc_serialize_binary_header(dc);
	memcpy(p->data, h, DC_PACKET_HEADER_SIZE);
	memcpy(p->data + offsetof(MsnDcContext, ack_id), dc->nonce, 16);

	msn_dc_enqueue_packet(dc, p);
}

static gboolean
msn_dc_verify_handshake(MsnDirectConn *dc, guint32 packet_length)
{
	guchar nonce[16];
	gchar  nonce_hash[37];

	if (packet_length != DC_PACKET_HEADER_SIZE)
		return FALSE;

	memcpy(nonce, dc->in_buffer + 4 + offsetof(MsnDcContext, ack_id), 16);

	msn_dc_calculate_nonce_hash(dc->nonce_type, nonce, nonce_hash);

	if (g_str_equal(dc->remote_nonce, nonce_hash)) {
		purple_debug_info("msn",
				"Received nonce %s from buddy request "
				"and calculated nonce %s from DC attempt. "
				"Nonces match, allowing direct connection\n",
				dc->remote_nonce, nonce_hash);
		return TRUE;
	} else {
		purple_debug_warning("msn",
				"Received nonce %s from buddy request "
				"and calculated nonce %s from DC attempt. "
				"Nonces don't match, ignoring direct connection\n",
				dc->remote_nonce, nonce_hash);
		return FALSE;
	}
}

static void
msn_dc_send_packet_cb(MsnDirectConnPacket *p)
{
	if (p->msg != NULL && p->msg->ack_cb != NULL)
		p->msg->ack_cb(p->msg, p->msg->ack_data);
}

void
msn_dc_enqueue_msg(MsnDirectConn *dc, MsnMessage *msg)
{
	MsnDirectConnPacket *p;
	guint32 length;

	length = msg->body_len + DC_PACKET_HEADER_SIZE;
	p = msn_dc_new_packet(length);

	memcpy(p->data, &msg->msnslp_header, DC_PACKET_HEADER_SIZE);
	memcpy(p->data + DC_PACKET_HEADER_SIZE, msg->body, msg->body_len);

	p->sent_cb = msn_dc_send_packet_cb;
	p->msg = msn_message_ref(msg);

	msn_dc_enqueue_packet(dc, p);
}

static int
msn_dc_process_packet(MsnDirectConn *dc, guint32 packet_length)
{
	g_return_val_if_fail(dc != NULL, DC_PROCESS_ERROR);

	switch (dc->state) {
	case DC_STATE_CLOSED:
		break;

	case DC_STATE_FOO:
		/* FOO message is always 4 bytes long */
		if (packet_length != 4 || memcmp(dc->in_buffer, "\4\0\0\0foo", 8) != 0)
			return DC_PROCESS_FALLBACK;

		dc->state = DC_STATE_HANDSHAKE;
		break;

	case DC_STATE_HANDSHAKE:
		if (!msn_dc_verify_handshake(dc, packet_length))
			return DC_PROCESS_FALLBACK;

		msn_dc_send_handshake_reply(dc);
		dc->state = DC_STATE_ESTABLISHED;

		msn_slpcall_session_init(dc->slpcall);
		dc->slpcall = NULL;
		break;

	case DC_STATE_HANDSHAKE_REPLY:
		if (!msn_dc_verify_handshake(dc, packet_length))
			return DC_PROCESS_FALLBACK;

		dc->state = DC_STATE_ESTABLISHED;

		msn_slpcall_session_init(dc->slpcall);
		dc->slpcall = NULL;
		break;

	case DC_STATE_ESTABLISHED:
		msn_slplink_process_msg(
			dc->slplink,
			&dc->header,
			dc->in_buffer + 4 + DC_PACKET_HEADER_SIZE,
			dc->header.length
		);

		/*
		if (dc->num_calls == 0) {
			msn_dc_destroy(dc);

			return DC_PROCESS_CLOSE;
		}
		*/
		break;
#if 0
		{
		guint64 file_size;
		int bytes_written;
		PurpleXfer *xfer;
		MsnSlpHeader *h = &dc->header;

		if (packet_length < DC_PACKET_HEADER_SIZE)
			return DC_TRANSFER_FALLBACK;

		/*
		 * TODO: MSN Messenger 7.0 sends BYE with flags 0x0000000 so we'll get rid of
		 * 0x1000000 bit but file data is always sent with flags 0x1000030 in both
		 * MSN Messenger and Live.*/
		switch (h->flags) {
		case 0x0000000:
		case 0x1000000:
			msn_dc_send_ack(dc);
			if (strncmp(dc->buffer, "BYE", 3) == 0) {
				/* Remote side cancelled the transfer. */
				purple_xfer_cancel_remote(dc->slpcall->xfer);
				return DC_TRANSFER_CANCELLED;
			}
			break;

		case 0x1000030:
			/* File data */
			xfer = dc->slpcall->xfer;
			file_size = purple_xfer_get_size(xfer);

			/* Packet sanity checks */
			if (	h->session_id != dc->slpcall->session_id ||
				h->offset >= file_size ||
				h->total_size != file_size ||
				h->length != packet_length - DC_PACKET_HEADER_SIZE ||
				h->offset + h->length > file_size) {

				purple_debug_warning("msn", "msn_dc_recv_process_packet_cb: packet range check error!\n");
				purple_xfer_cancel_local(dc->slpcall->xfer);
				return DC_TRANSFER_CANCELLED;
			}

			bytes_written = fwrite(dc->buffer, 1, h->length, xfer->dest_fp);
			if (bytes_written != h->length) {
				purple_debug_warning("msn", "msn_dc_recv_process_packet_cb: cannot write whole packet to file!\n");
				purple_xfer_cancel_local(dc->slpcall->xfer);
				return DC_TRANSFER_CANCELLED;
			}

			xfer->bytes_sent = (h->offset + h->length);
			xfer->bytes_remaining = h->total_size - xfer->bytes_sent;

			purple_xfer_update_progress(xfer);

			if (xfer->bytes_remaining == 0) {
				/* ACK only the last data packet */
				msn_dc_send_data_ack(dc);
				purple_xfer_set_completed(xfer, TRUE);
				dc->state = DC_STATE_BYE;
			}
			break;
		default:
			/*
			 * TODO: Packet with unknown flags. Should we ACK these?
			 */
			msn_dc_send_ack(dc);

			purple_debug_warning(
				"msn",
				"msn_dc_recv_process_packet_cb: received packet with unknown flags: 0x%08x\n",
				dc->header.flags
			);
		}
		break;
		}

	case DC_STATE_BYE:
		/* TODO: Check! */
		switch (dc->header.flags) {
		case 0x0000000:
		case 0x1000000:
			msn_dc_send_ack(dc);
			if (strncmp(dc->buffer, "BYE", 3) == 0) {
				dc->state = DC_STATE_COMPLETED;
				return DC_TRANSFER_COMPLETED;
			}
			break;

		default:
			/*
			 * TODO: Packet with unknown flags. Should we ACK these?
			 */
			msn_dc_send_ack(dc);
			purple_debug_warning(
				"msn",
				"msn_dc_recv_process_packet_cb: received packet with unknown flags: 0x%08x\n",
				dc->header.flags
			);
		}
		break;
#endif
	}

	return DC_PROCESS_OK;
}

static void
msn_dc_recv_cb(gpointer data, gint fd, PurpleInputCondition cond)
{
	MsnDirectConn *dc;
	int free_buf_space;
	int bytes_received;
	guint32 packet_length;

	g_return_if_fail(data != NULL);
	g_return_if_fail(fd != -1);

	dc = data;
	free_buf_space = dc->in_size - dc->in_pos;

	bytes_received = recv(fd, dc->in_buffer + dc->in_pos, free_buf_space, 0);
	if (bytes_received < 0) {
		if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
			return;

		purple_debug_warning("msn", "msn_dc_recv_cb: recv error\n");

		if(dc->state != DC_STATE_ESTABLISHED)
			msn_dc_fallback_to_p2p(dc);
		else
			msn_dc_destroy(dc);
		return;

	} else if (bytes_received == 0) {
		/* EOF. Remote side closed connection. */
		purple_debug_info("msn", "msn_dc_recv_cb: recv EOF\n");

		if(dc->state != DC_STATE_ESTABLISHED)
			msn_dc_fallback_to_p2p(dc);
		else
			msn_dc_destroy(dc);
		return;
	}

	dc->progress = TRUE;

	dc->in_pos += bytes_received;

	/* Wait for packet length */
	while (dc->in_pos >= 4) {
		packet_length = *((guint32*)dc->in_buffer);
		packet_length = GUINT32_FROM_LE(packet_length);

		if (packet_length > DC_MAX_PACKET_SIZE) {
			/* Oversized packet */
			purple_debug_warning("msn", "msn_dc_recv_cb: oversized packet received\n");
			return;
		}

		/* Wait for the whole packet to arrive */
		if (dc->in_pos < 4 + packet_length)
			return;

		if (dc->state != DC_STATE_FOO) {
			msn_dc_parse_binary_header(dc);
		}

		switch (msn_dc_process_packet(dc, packet_length)) {
		case DC_PROCESS_CLOSE:
			return;

		case DC_PROCESS_FALLBACK:
			purple_debug_warning("msn", "msn_dc_recv_cb: packet processing error, fall back to p2p\n");
			msn_dc_fallback_to_p2p(dc);
			return;

		}

		if (dc->in_pos > packet_length + 4) {
			memcpy(dc->in_buffer, dc->in_buffer + 4 + packet_length, dc->in_pos - packet_length - 4);
		}

		dc->in_pos -= packet_length + 4;
	}
}

#if 0
static gboolean
msn_dc_send_next_packet(MsnDirectConn *dc)
{
	MsnSlpMessage *msg;

	if(g_queue_is_empty(dc->out_queue))
		return TRUE;

	msg = g_queue_peek_head(dc->out_queue);
	msn_slplink_send_msgpart(dc->slplink, msg);



	PurpleXfer *xfer;
	int bytes_read;

	g_return_val_if_fail(dc != NULL, FALSE);
	g_return_val_if_fail(dc->slpcall != NULL, FALSE);

	xfer = dc->slpcall->xfer;

	bytes_read = fread(dc->buffer, 1, DC_MAX_BODY_SIZE, xfer->dest_fp);

	if (bytes_read > 0) {
		dc->header.session_id = dc->slpcall->session_id;
		/* Only increment seq. ID before sending BYE */
		dc->header.id = dc->slpcall->slplink->slp_seq_id;
		dc->header.offset = xfer->bytes_sent;
		dc->header.total_size = xfer->size;
		dc->header.length = bytes_read;
		dc->header.flags = 0x1000030;
		dc->header.ack_id = rand() % G_MAXUINT32;
		dc->header.ack_sub_id = 0;
		dc->header.ack_size = 0;

		msn_dc_send_packet(dc);

		xfer->bytes_sent += bytes_read;
		xfer->bytes_remaining -= bytes_read;
		purple_xfer_update_progress(xfer);

		if (xfer->bytes_remaining == 0) {
			purple_xfer_set_completed(xfer, TRUE);

			/* Increment seq. ID for the next BYE message */
			dc->slpcall->slplink->slp_seq_id++;
			dc->state = DC_STATE_DATA_ACK;
		}

	} else {
		/* File read error */
		purple_xfer_cancel_local(xfer);
		return FALSE;
	}

	return TRUE;
}

static int
msn_dc_send_process_packet_cb(MsnDirectConn *dc, guint32 packet_length)
{
	g_return_val_if_fail(dc != NULL, DC_TRANSFER_CANCELLED);

	switch (dc->state) {
	case DC_STATE_FOO: {
		if (packet_length != 4)
			return DC_TRANSFER_FALLBACK;

		if (memcmp(dc->in_buffer, "\4\0\0\0foo", 8) != 0)
			return DC_TRANSFER_FALLBACK;

		dc->state = DC_STATE_HANDSHAKE;
		break;
		}

	case DC_STATE_HANDSHAKE: {
		if (packet_length != DC_PACKET_HEADER_SIZE)
			return DC_TRANSFER_FALLBACK;

		/* TODO: Check! */
		msn_dc_send_handshake_reply(dc);
		dc->state = DC_STATE_TRANSFER;

		purple_xfer_set_request_denied_fnc(dc->slpcall->xfer, msn_dc_xfer_send_cancel);
		purple_xfer_set_cancel_send_fnc(dc->slpcall->xfer, msn_dc_xfer_send_cancel);
		purple_xfer_set_end_fnc(dc->slpcall->xfer, msn_dc_xfer_end);
		purple_xfer_start(dc->slpcall->xfer, -1, NULL, 0);
		break;
		}

	case DC_STATE_HANDSHAKE_REPLY:
		/* TODO: Check! */
		dc->state = DC_STATE_TRANSFER;
		break;

	case DC_STATE_TRANSFER: {
		switch (dc->header.flags) {
		case 0x0000000:
		case 0x1000000:
			msn_dc_send_ack(dc);
			if (strncmp(dc->buffer, "BYE", 3) == 0) {
				/* Remote side cancelled the transfer. */
				purple_xfer_cancel_remote(dc->slpcall->xfer);
				return DC_TRANSFER_CANCELLED;
			}
			break;
		}
		break;
		}

	case DC_STATE_DATA_ACK: {
		/* TODO: Check! */
		msn_dc_send_bye(dc);
		dc->state = DC_STATE_BYE_ACK;
		break;
		}

	case DC_STATE_BYE_ACK:
		/* TODO: Check! */
		dc->state = DC_STATE_COMPLETED;
		return DC_TRANSFER_COMPLETED;
	}

	return DC_TRANSFER_OK;
}
#endif

static gboolean
msn_dc_timeout(gpointer data)
{
	MsnDirectConn *dc = data;

	g_return_val_if_fail(dc != NULL, FALSE);

	if (dc->progress)
		dc->progress = FALSE;
	else
		msn_dc_destroy(dc);

	return TRUE;
}

static void
msn_dc_init(MsnDirectConn *dc)
{
	g_return_if_fail(dc != NULL);

	dc->in_size = DC_MAX_PACKET_SIZE + 4;
	dc->in_pos = 0;
	dc->in_buffer = g_malloc(dc->in_size);

	dc->recv_handle = purple_input_add(dc->fd, PURPLE_INPUT_READ, msn_dc_recv_cb, dc);
	dc->send_handle = purple_input_add(dc->fd, PURPLE_INPUT_WRITE, msn_dc_send_cb, dc);

	dc->timeout_handle = purple_timeout_add_seconds(DC_TIMEOUT, msn_dc_timeout, dc);
}

void
msn_dc_connected_to_peer_cb(gpointer data, gint fd, const gchar *error_msg)
{
	MsnDirectConn *dc = data;

	if (purple_debug_is_verbose())
		purple_debug_info("msn", "msn_dc_connected_to_peer_cb %p\n", dc);

	g_return_if_fail(dc != NULL);

	dc->connect_data = NULL;
	purple_timeout_remove(dc->connect_timeout_handle);
	dc->connect_timeout_handle = 0;

	dc->fd = fd;
	if (dc->fd != -1) {
		msn_dc_init(dc);
		msn_dc_send_foo(dc);
		msn_dc_send_handshake(dc);
		dc->state = DC_STATE_HANDSHAKE_REPLY;
	}
}

/*
 * This callback will be called when we're the server
 * and nobody has connected us in DC_CONNECT_TIMEOUT seconds
 */
static gboolean
msn_dc_incoming_connection_timeout_cb(gpointer data) {
	MsnDirectConn *dc = data;
	MsnSlpCall *slpcall = dc->slpcall;

	if (purple_debug_is_verbose())
		purple_debug_info("msn", "msn_dc_incoming_connection_timeout_cb %p\n", dc);

	g_return_val_if_fail(dc != NULL, FALSE);

	slpcall = dc->slpcall;
	g_return_val_if_fail(slpcall != NULL, FALSE);

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

	if (dc->listenfd_handle != 0) {
		purple_timeout_remove(dc->listenfd_handle);
		dc->listenfd_handle = 0;
	}

	if (dc->listenfd != -1) {
		purple_network_remove_port_mapping(dc->listenfd);
		close(dc->listenfd);
		dc->listenfd = -1;
	}

	msn_dc_destroy(dc);
	/* Start p2p file transfer */
	msn_slpcall_session_init(slpcall);

	return FALSE;
}

/*
 * This callback will be called when we're unable to connect to
 * the remote host in DC_CONNECT_TIMEOUT seconds.
 */
gboolean
msn_dc_outgoing_connection_timeout_cb(gpointer data)
{
	MsnDirectConn *dc = data;

	purple_debug_info("msn", "msn_dc_outgoing_connection_timeout_cb %p\n", dc);

	g_return_val_if_fail(dc != NULL, FALSE);

	if (dc->connect_timeout_handle != 0) {
		purple_timeout_remove(dc->connect_timeout_handle);
		dc->connect_timeout_handle = 0;
	}

	if (dc->connect_data != NULL) {
		purple_proxy_connect_cancel(dc->connect_data);
		dc->connect_data = NULL;
	}

	if (dc->ext_ip && dc->ext_port) {
		/* Try external IP/port if available. */
		dc->connect_data = purple_proxy_connect(
			NULL,
			dc->slpcall->slplink->session->account,
			dc->ext_ip,
			dc->ext_port,
			msn_dc_connected_to_peer_cb,
			dc
		);

		g_free(dc->ext_ip);
		dc->ext_ip = NULL;

		if (dc->connect_data) {
			dc->connect_timeout_handle = purple_timeout_add_seconds(
				DC_CONNECT_TIMEOUT,
				msn_dc_outgoing_connection_timeout_cb,
				dc
			);
		}

	} else {
		/*
		 * Both internal and external connection attempts are failed.
		 * Fall back to p2p transfer.
		 */
		MsnSlpCall	*slpcall = dc->slpcall;

		msn_dc_destroy(dc);
		/* Start p2p file transfer */
		msn_slpcall_session_init(slpcall);
	}

	return FALSE;
}

/*
 * This callback will be called when we're the server
 * and somebody has connected to us in DC_CONNECT_TIMEOUT seconds.
 */
static void
msn_dc_incoming_connection_cb(gpointer data, gint listenfd, PurpleInputCondition cond)
{
	MsnDirectConn *dc = data;

	if (purple_debug_is_verbose())
		purple_debug_info("msn", "msn_dc_incoming_connection_cb %p\n", dc);

	g_return_if_fail(dc != NULL);

	if (dc->connect_timeout_handle != 0) {
		purple_timeout_remove(dc->connect_timeout_handle);
		dc->connect_timeout_handle = 0;
	}

	if (dc->listenfd_handle != 0) {
		purple_input_remove(dc->listenfd_handle);
		dc->listenfd_handle = 0;
	}

	dc->fd = accept(listenfd, NULL, 0);

	purple_network_remove_port_mapping(dc->listenfd);
	close(dc->listenfd);
	dc->listenfd = -1;

	if (dc->fd != -1) {
		msn_dc_init(dc);
		dc->state = DC_STATE_FOO;
	}
}

void
msn_dc_listen_socket_created_cb(int listenfd, gpointer data)
{
	MsnDirectConn *dc = data;

	if (purple_debug_is_verbose())
		purple_debug_info("msn", "msn_dc_listen_socket_created_cb %p\n", dc);

	g_return_if_fail(dc != NULL);

	dc->listen_data = NULL;

	if (listenfd != -1) {
		const char *ext_ip;
		const char *int_ip;
		int port;

		ext_ip = purple_network_get_my_ip(listenfd);
		int_ip = purple_network_get_local_system_ip(listenfd);
		port = purple_network_get_port_from_fd(listenfd);

		dc->listenfd = listenfd;
		dc->listenfd_handle = purple_input_add(
			listenfd,
			PURPLE_INPUT_READ,
			msn_dc_incoming_connection_cb,
			dc
		);
		dc->connect_timeout_handle = purple_timeout_add_seconds(
			DC_CONNECT_TIMEOUT * 2, /* Internal + external connection attempts */
			msn_dc_incoming_connection_timeout_cb,
			dc
		);

		if (strcmp(int_ip, ext_ip) != 0) {
			dc->msg_body = g_strdup_printf(
				"Bridge: TCPv1\r\n"
				"Listening: true\r\n"
				"%sNonce: {%s}\r\n"
				"IPv4External-Addrs: %s\r\n"
				"IPv4External-Port: %d\r\n"
				"IPv4Internal-Addrs: %s\r\n"
				"IPv4Internal-Port: %d\r\n"
				"\r\n",

				dc->nonce_type != DC_NONCE_PLAIN ? "Hashed-" : "",
				dc->nonce_hash,
				ext_ip,
				port,
				int_ip,
				port
			);

		} else {
			dc->msg_body = g_strdup_printf(
				"Bridge: TCPv1\r\n"
				"Listening: true\r\n"
				"%sNonce: {%s}\r\n"
				"IPv4External-Addrs: %s\r\n"
				"IPv4External-Port: %d\r\n"
				"\r\n",

				dc->nonce_type != DC_NONCE_PLAIN ? "Hashed-" : "",
				dc->nonce_hash,
				ext_ip,
				port
			);
		}

		if (dc->slpcall->wait_for_socket) {
			if (dc->send_connection_info_msg_cb != NULL)
				dc->send_connection_info_msg_cb(dc);

			dc->slpcall->wait_for_socket = FALSE;
		}
	}
}