/********************************************************************\ * 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; }