/********************************************************************\
* BitlBee -- An IRC to other IM-networks gateway                     *
*                                                                    *
* Copyright 2008 Uli Meis					     *
* Copyright 2006 Marijn Kruisselbrink and others                     *
\********************************************************************/

/* MSN module - File transfer support             */

/*
 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 with
 the Debian GNU/Linux distribution in /usr/share/common-licenses/GPL;
 if not, write to the Free Software Foundation, Inc., 59 Temple Place,
 Suite 330, Boston, MA  02111-1307  USA
 */

#include "bitlbee.h"
#include "invitation.h"
#include "msn.h"
#include "lib/ftutil.h"

#ifdef debug
#undef debug
#endif
#define debug(msg...) log_message( LOGLVL_INFO, msg )

static void msn_ftp_free( file_transfer_t *file );
static void msn_ftpr_accept( file_transfer_t *file );
static void msn_ftp_finished( file_transfer_t *file );
static void msn_ftp_canceled( file_transfer_t *file, char *reason );
static gboolean msn_ftpr_write_request( file_transfer_t *file );

static gboolean msn_ftp_connected( gpointer data, gint fd, b_input_condition cond );
static gboolean msn_ftp_read( gpointer data, gint fd, b_input_condition cond );
gboolean msn_ftps_write( file_transfer_t *file, char *buffer, unsigned int len );

/*
 * Vararg wrapper for imcb_file_canceled().
 */
gboolean msn_ftp_abort( file_transfer_t *file, char *format, ... )
{
        va_list params;
        va_start( params, format );
        char error[128];

        if( vsnprintf( error, 128, format, params ) < 0 )
                sprintf( error, "internal error parsing error string (BUG)" );
        va_end( params );
	imcb_file_canceled( file, error );
	return FALSE;
}

/* very useful */
#define ASSERTSOCKOP(op, msg) \
	if( (op) == -1 ) \
		return msn_ftp_abort( file , msg ": %s", strerror( errno ) );

void msn_ftp_invitation_cmd( struct im_connection *ic, char *who, int cookie, char *icmd,
			     char *trailer )
{
	struct msn_message *m = g_new0( struct msn_message, 1 );
	
	m->text = g_strdup_printf( "%s"
		    "Invitation-Command: %s\r\n"
		    "Invitation-Cookie: %u\r\n"
		    "%s",
		    MSN_INVITE_HEADERS,
		    icmd,
		    cookie,
		    trailer);
	
	m->who = g_strdup( who );

	msn_sb_write_msg( ic, m );
}

void msn_ftp_cancel_invite( struct im_connection *ic, char *who,  int cookie, char *code )
{
	char buf[64];

	g_snprintf( buf, sizeof( buf ), "Cancel-Code: %s\r\n", code );
	msn_ftp_invitation_cmd( ic, who, cookie, "CANCEL", buf );
}

void msn_ftp_transfer_request( struct im_connection *ic, file_transfer_t *file, char *who )
{
	unsigned int cookie = time( NULL ); /* TODO: randomize */
	char buf[2048];

	msn_filetransfer_t *msn_file = g_new0( msn_filetransfer_t, 1 );
	file->data = msn_file;
	file->free = msn_ftp_free;
	file->canceled = msn_ftp_canceled;
	file->write = msn_ftps_write;
	msn_file->md = ic->proto_data;
	msn_file->invite_cookie = cookie;
	msn_file->handle = g_strdup( who );
	msn_file->dcc = file;
	msn_file->md->filetransfers = g_slist_prepend( msn_file->md->filetransfers, msn_file->dcc );
	msn_file->fd = -1;
	msn_file->sbufpos = 3;

	g_snprintf( buf, sizeof( buf ), 
		"Application-Name: File Transfer\r\n"
		"Application-GUID: {5D3E02AB-6190-11d3-BBBB-00C04F795683}\r\n"
		"Application-File: %s\r\n"
		"Application-FileSize: %zd\r\n",
		file->file_name,
		file->file_size);

	msn_ftp_invitation_cmd( msn_file->md->ic, msn_file->handle, cookie, "INVITE", buf );

	imcb_file_recv_start( file );
}

void msn_invitation_invite( struct msn_switchboard *sb, char *handle, unsigned int cookie, char *body, int blen )
{
	char *itype = msn_findheader( body, "Application-GUID:", blen );
	char *name, *size, *invitecookie, *reject = NULL;
	user_t *u;
	size_t isize;
	file_transfer_t *file;
	
	if( !itype || strcmp( itype, "{5D3E02AB-6190-11d3-BBBB-00C04F795683}" ) != 0 ) {
		/* Don't know what that is - don't care */
		char *iname = msn_findheader( body, "Application-Name:", blen );
		imcb_log( sb->ic, "Received unknown MSN invitation %s (%s) from %s",
			  itype ? : "with no GUID", iname ? iname : "no application name", handle );
		g_free( iname );
		reject = "REJECT_NOT_INSTALLED";
	} else if ( 
		!( name = msn_findheader( body, "Application-File:", blen )) || 
		!( size = msn_findheader( body, "Application-FileSize:", blen )) || 
		!( invitecookie = msn_findheader( body, "Invitation-Cookie:", blen)) ||
		!( isize = atoll( size ) ) ) { 
		imcb_log( sb->ic, "Received corrupted transfer request from %s"
			  "(name=%s, size=%s, invitecookie=%s)",
			  handle, name, size, invitecookie );
		reject = "REJECT";
	} else if ( !( u = user_findhandle( sb->ic, handle ) ) ) {
		imcb_log( sb->ic, "Error in parsing transfer request, User '%s'"
			  "is not in contact list", handle );
		reject = "REJECT";
	} else if ( !( file = imcb_file_send_start( sb->ic, handle, name, isize ) ) ) {
		imcb_log( sb->ic, "Error initiating transfer for request from %s for %s",
			  handle, name );
		reject = "REJECT";
	} else {
		msn_filetransfer_t *msn_file = g_new0( msn_filetransfer_t, 1 );
		file->data = msn_file;
		file->accept = msn_ftpr_accept;
		file->free = msn_ftp_free;
		file->finished = msn_ftp_finished;
		file->canceled = msn_ftp_canceled;
		file->write_request = msn_ftpr_write_request;
		msn_file->md = sb->ic->proto_data;
		msn_file->invite_cookie = cookie;
		msn_file->handle = g_strdup( handle );
		msn_file->dcc = file;
		msn_file->md->filetransfers = g_slist_prepend( msn_file->md->filetransfers, msn_file->dcc );
		msn_file->fd = -1;
	}

	if( reject )
		msn_ftp_cancel_invite( sb->ic, sb->who, cookie, reject );

	g_free( name );
	g_free( size );
	g_free( invitecookie );
	g_free( itype );
}

msn_filetransfer_t* msn_find_filetransfer( struct msn_data *md, unsigned int cookie, char *handle )
{
	GSList *l;
	
	for( l = md->filetransfers; l; l = l->next ) {
		msn_filetransfer_t *file = ( (file_transfer_t*) l->data )->data;
		if( file->invite_cookie == cookie && strcmp( handle, file->handle ) == 0 ) {
			return file;
		}
	}
	return NULL;
}

gboolean msn_ftps_connected( gpointer data, gint fd, b_input_condition cond )
{
	file_transfer_t *file = data;
	msn_filetransfer_t *msn_file = file->data;
	struct sockaddr_storage clt_addr;
	socklen_t ssize = sizeof( clt_addr );
	
	debug( "Connected to MSNFTP client" );
	
	ASSERTSOCKOP( msn_file->fd = accept( fd, (struct sockaddr *) &clt_addr, &ssize ), "Accepting connection" );

	closesocket( fd );
	fd = msn_file->fd;
	sock_make_nonblocking( fd );

	msn_file->r_event_id = b_input_add( fd, B_EV_IO_READ, msn_ftp_read, file );

	return FALSE;
}

void msn_invitations_accept( msn_filetransfer_t *msn_file, struct msn_switchboard *sb, char *handle, unsigned int cookie, char *body, int blen )
{
	file_transfer_t *file = msn_file->dcc;
	char buf[1024];
	unsigned int acookie = time ( NULL );
	char host[HOST_NAME_MAX+1];
	char port[6];
	char *errmsg;

	msn_file->auth_cookie = acookie;

	if( ( msn_file->fd = ft_listen( NULL, host, port, FALSE, &errmsg ) ) == -1 ) {
		msn_ftp_abort( file, "Failed to listen locally, check your ft_listen setting in bitlbee.conf: %s", errmsg );
		return;
	}

	msn_file->r_event_id = b_input_add( msn_file->fd, B_EV_IO_READ, msn_ftps_connected, file );

	g_snprintf( buf, sizeof( buf ),
		    "IP-Address: %s\r\n"
		    "Port: %s\r\n"
		    "AuthCookie: %d\r\n"
		    "Launch-Application: FALSE\r\n"
		    "Request-Data: IP-Address:\r\n\r\n",
		    host,
		    port,
		    msn_file->auth_cookie );

	msn_ftp_invitation_cmd( msn_file->md->ic, handle, msn_file->invite_cookie, "ACCEPT", buf );
}

void msn_invitationr_accept( msn_filetransfer_t *msn_file, struct msn_switchboard *sb, char *handle, unsigned int cookie, char *body, int blen ) {
	file_transfer_t *file = msn_file->dcc;
	char *authcookie, *ip, *port;

	if( !( authcookie = msn_findheader( body, "AuthCookie:", blen ) ) ||
	    !( ip = msn_findheader( body, "IP-Address:", blen ) ) ||
	    !( port = msn_findheader( body, "Port:", blen ) ) ) {
		msn_ftp_abort( file, "Received invalid accept reply" );
	} else if( 
		( msn_file->fd = proxy_connect( ip, atoi( port ), msn_ftp_connected, file ) )
		< 0 ) {
			msn_ftp_abort( file, "Error connecting to MSN client" );
	} else
		msn_file->auth_cookie = strtoul( authcookie, NULL, 10 );

	g_free( authcookie );
	g_free( ip );
	g_free( port );
}

void msn_invitation_accept( struct msn_switchboard *sb, char *handle, unsigned int cookie, char *body, int blen )
{
	msn_filetransfer_t *msn_file = msn_find_filetransfer( sb->ic->proto_data, cookie, handle );
	file_transfer_t *file = msn_file ? msn_file->dcc : NULL;
	
	if( !msn_file  )
		imcb_log( sb->ic, "Received invitation ACCEPT message for unknown invitation (already aborted?)" );
	else if( file->sending ) 
		msn_invitations_accept( msn_file, sb, handle, cookie, body, blen );
	else
		msn_invitationr_accept( msn_file, sb, handle, cookie, body, blen );
}

void msn_invitation_cancel( struct msn_switchboard *sb, char *handle, unsigned int cookie, char *body, int blen )
{
	msn_filetransfer_t *msn_file = msn_find_filetransfer( sb->ic->proto_data, cookie, handle );
	
	if( !msn_file )
		imcb_log( sb->ic, "Received invitation CANCEL message for unknown invitation (already aborted?)" );
	else
		msn_ftp_abort( msn_file->dcc, msn_findheader( body, "Cancel-Code:", blen ) );
}

int msn_ftp_write( file_transfer_t *file, char *format, ... )
{
	msn_filetransfer_t *msn_file = file->data;
	va_list params;
	int st;
	char *s;
	
	va_start( params, format );
	s = g_strdup_vprintf( format, params );
	va_end( params );
	
	st = write( msn_file->fd, s, strlen( s ) );
	if( st != strlen( s ) )
		return msn_ftp_abort( file, "Error sending data over MSNFTP connection: %s",
				strerror( errno ) );
	
	g_free( s );
	return 1;
}

gboolean msn_ftp_connected( gpointer data, gint fd, b_input_condition cond )
{
	file_transfer_t *file = data;
	msn_filetransfer_t *msn_file = file->data;
	
	debug( "Connected to MSNFTP server, starting authentication" );
	if( !msn_ftp_write( file, "VER MSNFTP\r\n" ) ) 
		return FALSE;
	
	sock_make_nonblocking( msn_file->fd );
	msn_file->r_event_id = b_input_add( msn_file->fd, B_EV_IO_READ, msn_ftp_read, file );
	
	return FALSE;
}

gboolean msn_ftp_handle_command( file_transfer_t *file, char* line )
{
	msn_filetransfer_t *msn_file = file->data;
	char **cmd = msn_linesplit( line );
	int count = 0;
	if( cmd[0] ) while( cmd[++count] );
	
	if( count < 1 )
		return msn_ftp_abort( file, "Missing command in MSNFTP communication" );
	
	if( strcmp( cmd[0], "VER" ) == 0 ) {
		if( strcmp( cmd[1], "MSNFTP" ) != 0 )
			return msn_ftp_abort( file, "Unsupported filetransfer protocol: %s", cmd[1] );
		if( file->sending )
			msn_ftp_write( file, "VER MSNFTP\r\n" );
		else 
			msn_ftp_write( file, "USR %s %u\r\n", msn_file->md->ic->acc->user, msn_file->auth_cookie );
	} else if( strcmp( cmd[0], "FIL" ) == 0 ) {
		if( strtoul( cmd[1], NULL, 10 ) != file->file_size )
			return msn_ftp_abort( file, "FIL reply contains a different file size than the size in the invitation" );
		msn_ftp_write( file, "TFR\r\n" );
		msn_file->status |= MSN_TRANSFER_RECEIVING;
	} else if( strcmp( cmd[0], "USR" ) == 0 ) {
		if( ( strcmp( cmd[1], msn_file->handle ) != 0 ) ||
		    ( strtoul( cmd[2], NULL, 10 ) != msn_file->auth_cookie ) )
			msn_ftp_abort( file, "Authentication failed. "
				"Expected handle: %s (got %s), cookie: %u (got %s)",
				msn_file->handle, cmd[1],
				msn_file->auth_cookie, cmd[2] );
		msn_ftp_write( file, "FIL %zu\r\n", file->file_size);
	} else if( strcmp( cmd[0], "TFR" ) == 0 ) {
		file->write_request( file );
	} else if( strcmp( cmd[0], "BYE" ) == 0 ) {
		unsigned int retcode = count > 1 ? atoi(cmd[1]) : 1;

		if( ( retcode==16777989 ) || ( retcode==16777987 ) )
			imcb_file_finished( file );
		else if( retcode==2147942405 )
			imcb_file_canceled( file, "Failure: receiver is out of disk space" );
		else if( retcode==2164261682 )
			imcb_file_canceled( file, "Failure: receiver cancelled the transfer" );
		else if( retcode==2164261683 )
			imcb_file_canceled( file, "Failure: sender has cancelled the transfer" );
		else if( retcode==2164261694 )
			imcb_file_canceled( file, "Failure: connection is blocked" );
		else {
			char buf[128];

			sprintf( buf, "Failure: unknown BYE code: %d", retcode);
			imcb_file_canceled( file, buf );
		}
	} else if( strcmp( cmd[0], "CCL" ) == 0 ) {
		imcb_file_canceled( file, "Failure: receiver cancelled the transfer" );
	} else {
		msn_ftp_abort( file, "Received invalid command %s from msn client", cmd[0] );
	}
	return TRUE;
}

gboolean msn_ftp_send( gpointer data, gint fd, b_input_condition cond )
{
	file_transfer_t *file = data;
	msn_filetransfer_t *msn_file = file->data;

	msn_file->w_event_id = 0;

	file->write_request( file );

	return FALSE;
}

/*
 * This should only be called if we can write, so just do it.
 * Add a write watch so we can write more during the next cycle (if possible).
 * This got a bit complicated because (at least) amsn expects packets of size 2045.
 */
gboolean msn_ftps_write( file_transfer_t *file, char *buffer, unsigned int len )
{
	msn_filetransfer_t *msn_file = file->data;
        int ret, overflow;

	/* what we can't send now */
	overflow = msn_file->sbufpos + len - MSNFTP_PSIZE;

	/* append what we can do the send buffer */
	memcpy( msn_file->sbuf + msn_file->sbufpos, buffer, MIN( len, MSNFTP_PSIZE - msn_file->sbufpos ) );
	msn_file->sbufpos += MIN( len, MSNFTP_PSIZE - msn_file->sbufpos );

	/* if we don't have enough for a full packet and there's more wait for it */
	if( ( msn_file->sbufpos < MSNFTP_PSIZE ) && 
	    ( msn_file->data_sent + msn_file->sbufpos - 3 < file->file_size ) ) {
		if( !msn_file->w_event_id )
			msn_file->w_event_id = b_input_add( msn_file->fd, B_EV_IO_WRITE, msn_ftp_send, file );
		return TRUE;
	}

	/* Accumulated enough data, lets send something out */

	msn_file->sbuf[0] = 0;
	msn_file->sbuf[1] = ( msn_file->sbufpos - 3 ) & 0xff;
	msn_file->sbuf[2] = ( ( msn_file->sbufpos - 3 ) >> 8 ) & 0xff;

        ASSERTSOCKOP( ret = send( msn_file->fd, msn_file->sbuf, msn_file->sbufpos, 0 ), "Sending" );

        msn_file->data_sent += ret - 3;

        /* TODO: this should really not be fatal */
        if( ret < msn_file->sbufpos )
                return msn_ftp_abort( file, "send() sent %d instead of %d (send buffer full!)", ret, msn_file->sbufpos );

	msn_file->sbufpos = 3;

	if( overflow > 0 ) {
		while( overflow > ( MSNFTP_PSIZE - 3 ) ) {
			if( !msn_ftps_write( file, buffer + len - overflow, MSNFTP_PSIZE - 3 ) )
				return FALSE;
			overflow -= MSNFTP_PSIZE - 3;
		}
		return msn_ftps_write( file, buffer + len - overflow, overflow );
	}

	if( msn_file->data_sent == file->file_size ) {
		if( msn_file->w_event_id ) {
			b_event_remove( msn_file->w_event_id );
			msn_file->w_event_id = 0;
		}
	} else {
		/* we might already be listening if this is data from an overflow */
		if( !msn_file->w_event_id )
			msn_file->w_event_id = b_input_add( msn_file->fd, B_EV_IO_WRITE, msn_ftp_send, file );
	}

        return TRUE;
}

/* Binary part of the file transfer protocol */
gboolean msn_ftpr_read( file_transfer_t *file ) 
{
	msn_filetransfer_t *msn_file = file->data;
	int st;
	unsigned char buf[3];

	if( msn_file->data_remaining ) {
		msn_file->r_event_id = 0;

		ASSERTSOCKOP( st = read( msn_file->fd, file->buffer, MIN( sizeof( file->buffer ), msn_file->data_remaining ) ), "Receiving" );

		if( st == 0 )
			return msn_ftp_abort( file, "Remote end closed connection");

		msn_file->data_sent += st;

		msn_file->data_remaining -= st;

		file->write( file, file->buffer, st );

		if( msn_file->data_sent >= file->file_size )
			imcb_file_finished( file );

		return FALSE;
	} else {
		ASSERTSOCKOP( st = read( msn_file->fd, buf, 1 ), "Receiving" );
		if( st == 0 ) {
			return msn_ftp_abort( file, "read returned EOF while reading data header from msn client" );
		} else if( buf[0] == '\r' || buf[0] == '\n' ) {
			debug( "Discarding extraneous newline" );
		} else if( buf[0] != 0 ) {
			msn_ftp_abort( file, "Remote end canceled the transfer");
			/* don't really care about these last 2 (should be 0,0) */
			read( msn_file->fd, buf, 2 );
			return FALSE;
		} else {
			unsigned int size;
			ASSERTSOCKOP( st = read( msn_file->fd, buf, 2 ), "Receiving" );
			if( st < 2 )
				return msn_ftp_abort( file, "read returned EOF while reading data header from msn client" );

			size = buf[0] + ((unsigned int) buf[1] << 8);
			msn_file->data_remaining = size;
		}
	}
	return TRUE;
}

/* Text mode part of the file transfer protocol */
gboolean msn_ftp_txtproto( file_transfer_t *file )
{
	msn_filetransfer_t *msn_file = file->data;
	int i = msn_file->tbufpos, st;
	char *tbuf = msn_file->tbuf;

	ASSERTSOCKOP( st = read( msn_file->fd, 
				 tbuf + msn_file->tbufpos, 
				 sizeof( msn_file->tbuf ) - msn_file->tbufpos ),
				 "Receiving" );

	if( st == 0 )
		return msn_ftp_abort( file, "read returned EOF while reading text from msn client" );

	msn_file->tbufpos += st;

	do {
		for( ;i < msn_file->tbufpos; i++ ) {
			if( tbuf[i] == '\n' || tbuf[i] == '\r' ) {
				tbuf[i] = '\0';
				if( i > 0 )
					msn_ftp_handle_command( file, tbuf );
				else
					while( tbuf[i] == '\n' || tbuf[i] == '\r' ) i++;
				memmove( tbuf, tbuf + i + 1, msn_file->tbufpos - i - 1 );
				msn_file->tbufpos -= i + 1;
				i = 0;
				break;
			}
		}
	} while ( i < msn_file->tbufpos );

	if( msn_file->tbufpos == sizeof( msn_file->tbuf ) )
		return msn_ftp_abort( file, 
				      "Line exceeded %d bytes in text protocol", 
				      sizeof( msn_file->tbuf ) );
	return TRUE;
}

gboolean msn_ftp_read( gpointer data, gint fd, b_input_condition cond )
{
	file_transfer_t *file = data;
	msn_filetransfer_t *msn_file = file->data;
	
	if( msn_file->status & MSN_TRANSFER_RECEIVING )
		return msn_ftpr_read( file );
	else
		return msn_ftp_txtproto( file );
}

void msn_ftp_free( file_transfer_t *file )
{
	msn_filetransfer_t *msn_file = file->data;
	
	if( msn_file->r_event_id )
		b_event_remove( msn_file->r_event_id );

	if( msn_file->w_event_id )
		b_event_remove( msn_file->w_event_id );

	if( msn_file->fd != -1 )
		closesocket( msn_file->fd );

	msn_file->md->filetransfers = g_slist_remove( msn_file->md->filetransfers, msn_file->dcc );
	
	g_free( msn_file->handle );
	
	g_free( msn_file );
}

void msn_ftpr_accept( file_transfer_t *file )
{
	msn_filetransfer_t *msn_file = file->data;

	msn_ftp_invitation_cmd( msn_file->md->ic, msn_file->handle, msn_file->invite_cookie, "ACCEPT", 
				"Launch-Application: FALSE\r\n" 
				"Request-Data: IP-Address:\r\n");
}

void msn_ftp_finished( file_transfer_t *file )
{
	msn_ftp_write( file, "BYE 16777989\r\n" );
}

void msn_ftp_canceled( file_transfer_t *file, char *reason )
{
	msn_filetransfer_t *msn_file = file->data;

	msn_ftp_cancel_invite( msn_file->md->ic, msn_file->handle, 
			       msn_file->invite_cookie, 
			       file->status & FT_STATUS_TRANSFERRING ? 
					"FTTIMEOUT" : 
					"FAIL" );

	imcb_log( msn_file->md->ic, "File transfer aborted: %s", reason );
}

gboolean msn_ftpr_write_request( file_transfer_t *file )
{
	msn_filetransfer_t *msn_file = file->data;
	if( msn_file->r_event_id != 0 ) {
		msn_ftp_abort( file, 
					"BUG in MSN file transfer:"
					"write_request called when"
					"already watching for input" );
		return FALSE;
	}

	msn_file->r_event_id = 
		b_input_add( msn_file->fd, B_EV_IO_READ, msn_ftp_read, file );

	return TRUE;
}
