diff options
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | conf.c | 1 | ||||
-rw-r--r-- | conf.h | 1 | ||||
-rw-r--r-- | dcc.c | 534 | ||||
-rw-r--r-- | dcc.h | 125 | ||||
-rw-r--r-- | irc.h | 1 | ||||
-rw-r--r-- | protocols/ft.h | 153 | ||||
-rw-r--r-- | protocols/jabber/Makefile | 2 | ||||
-rw-r--r-- | protocols/jabber/iq.c | 28 | ||||
-rw-r--r-- | protocols/jabber/jabber.h | 58 | ||||
-rw-r--r-- | protocols/jabber/jabber_util.c | 6 | ||||
-rw-r--r-- | protocols/jabber/si.c | 246 | ||||
-rw-r--r-- | protocols/jabber/stream.c | 593 | ||||
-rw-r--r-- | protocols/nogaim.h | 1 | ||||
-rw-r--r-- | root_commands.c | 66 |
15 files changed, 1795 insertions, 24 deletions
@@ -9,8 +9,8 @@ -include Makefile.settings # Program variables -objects = account.o bitlbee.o conf.o crypting.o help.o ipc.o irc.o irc_commands.o log.o nick.o query.o root_commands.o set.o storage.o $(STORAGE_OBJS) unix.o user.o -headers = account.h bitlbee.h commands.h conf.h config.h crypting.h help.h ipc.h irc.h log.h nick.h query.h set.h sock.h storage.h user.h lib/events.h lib/http_client.h lib/ini.h lib/md5.h lib/misc.h lib/proxy.h lib/sha1.h lib/ssl_client.h lib/url.h protocols/nogaim.h +objects = account.o bitlbee.o conf.o crypting.o help.o ipc.o irc.o irc_commands.o log.o nick.o query.o root_commands.o set.o storage.o $(STORAGE_OBJS) unix.o user.o dcc.o +headers = account.h bitlbee.h commands.h conf.h config.h crypting.h help.h ipc.h irc.h log.h nick.h query.h set.h sock.h storage.h user.h dcc.h lib/events.h lib/http_client.h lib/ini.h lib/md5.h lib/misc.h lib/proxy.h lib/sha1.h lib/ssl_client.h lib/url.h protocols/nogaim.h protocols/ft.h subdirs = lib protocols # Expansion of variables @@ -62,6 +62,7 @@ conf_t *conf_load( int argc, char *argv[] ) conf->motdfile = g_strdup( ETCDIR "/motd.txt" ); conf->ping_interval = 180; conf->ping_timeout = 300; + conf->max_filetransfer_size = G_MAXUINT; proxytype = 0; i = conf_loadini( conf, CONF_FILE ); @@ -48,6 +48,7 @@ typedef struct conf char **migrate_storage; int ping_interval; int ping_timeout; + size_t max_filetransfer_size; } conf_t; G_GNUC_MALLOC conf_t *conf_load( int argc, char *argv[] ); @@ -0,0 +1,534 @@ +/********************************************************************\ +* BitlBee -- An IRC to other IM-networks gateway * +* * +* Copyright 2007 Uli Meis <a.sporto+bee@gmail.com> * +\********************************************************************/ + +/* + 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 +*/ + +#define BITLBEE_CORE +#include "bitlbee.h" +#include "ft.h" +#include "dcc.h" +#include <poll.h> +#include <netinet/tcp.h> + +/* + * Since that might be confusing a note on naming: + * + * Generic dcc functions start with + * + * dcc_ + * + * ,methods specific to DCC SEND start with + * + * dccs_ + * + * . Since we can be on both ends of a DCC SEND, + * functions specific to one end are called + * + * dccs_send and dccs_recv + * + * ,respectively. + */ + + +/* + * used to generate a unique local transfer id the user + * can use to reject/cancel transfers + */ +unsigned int local_transfer_id=1; + +/* + * just for debugging the nr. of chunks we received from im-protocols and the total data + */ +unsigned int receivedchunks=0, receiveddata=0; + +/* + * If using DCC SEND AHEAD this value will be set before the first transfer starts. + * Not that in this case it degenerates to the maximum message size to send() and + * has nothing to do with packets. + */ +#ifdef DCC_SEND_AHEAD +int max_packet_size = DCC_PACKET_SIZE; +#else +int max_packet_size = 0; +#endif + +static void dcc_finish( file_transfer_t *file ); +static void dcc_close( file_transfer_t *file ); +gboolean dccs_send_proto( gpointer data, gint fd, b_input_condition cond ); +gboolean dcc_listen( dcc_file_transfer_t *df, struct sockaddr_storage **saddr_ptr ); +int dccs_send_request( struct dcc_file_transfer *df, char *user_nick, struct sockaddr_storage *saddr ); + +/* As defined in ft.h */ +file_transfer_t *imcb_file_send_start( struct im_connection *ic, char *handle, char *file_name, size_t file_size ) +{ + user_t *u = user_findhandle( ic, handle ); + /* one could handle this more intelligent like imcb_buddy_msg. + * can't call it directly though cause it does some wrapping. + * Maybe give imcb_buddy_msg a parameter NO_WRAPPING? */ + if (!u) return NULL; + + return dccs_send_start( ic, u->nick, file_name, file_size ); +}; + +/* As defined in ft.h */ +void imcb_file_canceled( file_transfer_t *file, char *reason ) +{ + if( file->canceled ) + file->canceled( file, reason ); + + dcc_close( file ); +} + +/* As defined in ft.h */ +gboolean imcb_file_write( file_transfer_t *file, gpointer data, size_t data_size ) +{ + return dccs_send_write( file, data, data_size ); +} + +/* This is where the sending magic starts... */ +file_transfer_t *dccs_send_start( struct im_connection *ic, char *user_nick, char *file_name, size_t file_size ) +{ + file_transfer_t *file; + dcc_file_transfer_t *df; + struct sockaddr_storage **saddr; + + if( file_size > global.conf->max_filetransfer_size ) + return NULL; + + /* alloc stuff */ + file = g_new0( file_transfer_t, 1 ); + file->priv = df = g_new0( dcc_file_transfer_t, 1); + file->file_size = file_size; + file->file_name = g_strdup( file_name ); + file->local_id = local_transfer_id++; + df->ic = ic; + df->ft = file; + + /* listen and request */ + if( !dcc_listen( df, saddr ) || + !dccs_send_request( df, user_nick, *saddr ) ) + return NULL; + + g_free( *saddr ); + + /* watch */ + df->watch_in = b_input_add( df->fd, GAIM_INPUT_READ, dccs_send_proto, df ); + + df->ic->irc->file_transfers = g_slist_prepend( df->ic->irc->file_transfers, file ); + + return file; +} + +/* Used pretty much everywhere in the code to abort a transfer */ +gboolean dcc_abort( dcc_file_transfer_t *df, char *reason, ... ) +{ + file_transfer_t *file = df->ft; + va_list params; + va_start( params, reason ); + char *msg = g_strdup_vprintf( reason, params ); + va_end( params ); + + file->status |= FT_STATUS_CANCELED; + + if( file->canceled ) + file->canceled( file, msg ); + else + imcb_log( df->ic, "DCC transfer aborted: %s", msg ); + + g_free( msg ); + + dcc_close( df->ft ); + + return FALSE; +} + +/* used extensively for socket operations */ +#define ASSERTSOCKOP(op, msg) \ + if( (op) == -1 ) \ + return dcc_abort( df , msg ": %s", strerror( errno ) ); + +/* Creates the "DCC SEND" line and sends it to the server */ +int dccs_send_request( struct dcc_file_transfer *df, char *user_nick, struct sockaddr_storage *saddr ) +{ + char ipaddr[INET6_ADDRSTRLEN]; + const void *netaddr; + int port; + char *cmd; + + if( saddr->ss_family == AF_INET ) + { + struct sockaddr_in *saddr_ipv4 = ( struct sockaddr_in *) saddr; + + /* + * this is so ridiculous. We're supposed to convert the address to + * host byte order!!! Let's exclude anyone running big endian just + * for the fun of it... + */ + sprintf( ipaddr, "%d", + htonl( saddr_ipv4->sin_addr.s_addr ) ); + port = saddr_ipv4->sin_port; + } else + { + struct sockaddr_in6 *saddr_ipv6 = ( struct sockaddr_in6 *) saddr; + + netaddr = &saddr_ipv6->sin6_addr.s6_addr; + port = saddr_ipv6->sin6_port; + + /* + * Didn't find docs about this, but it seems that's the way irssi does it + */ + if( !inet_ntop( saddr->ss_family, netaddr, ipaddr, sizeof( ipaddr ) ) ) + return dcc_abort( df, "inet_ntop failed: %s", strerror( errno ) ); + } + + port = ntohs( port ); + + cmd = g_strdup_printf( "\001DCC SEND %s %s %u %zu\001", + df->ft->file_name, ipaddr, port, df->ft->file_size ); + + if ( !irc_msgfrom( df->ic->irc, user_nick, cmd ) ) + return dcc_abort( df, "couldn't send 'DCC SEND' message to %s", user_nick ); + + g_free( cmd ); + + /* message is sortof redundant cause the users client probably informs him about that. remove? */ + imcb_log( df->ic, "Transferring file %s: Chose local address %s for DCC connection", df->ft->file_name, ipaddr ); + + return TRUE; +} + +/* + * Creates a listening socket and returns it in saddr_ptr. + */ +gboolean dcc_listen( dcc_file_transfer_t *df, struct sockaddr_storage **saddr_ptr ) +{ + file_transfer_t *file = df->ft; + struct sockaddr_storage *saddr; + int fd; + char hostname[ HOST_NAME_MAX + 1 ]; + struct addrinfo hints, *rp; + socklen_t ssize = sizeof( struct sockaddr_storage ); + + /* won't be long till someone asks for this to be configurable :) */ + + ASSERTSOCKOP( gethostname( hostname, sizeof( hostname ) ), "gethostname()" ); + + memset( &hints, 0, sizeof( struct addrinfo ) ); + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_NUMERICSERV; + + if ( getaddrinfo( hostname, "0", &hints, &rp ) != 0 ) + return dcc_abort( df, "getaddrinfo()" ); + + saddr = g_new( struct sockaddr_storage, 1 ); + + *saddr_ptr = saddr; + + memcpy( saddr, rp->ai_addr, rp->ai_addrlen ); + + ASSERTSOCKOP( fd = df->fd = socket( saddr->ss_family, SOCK_STREAM, 0 ), "Opening socket" ); + + ASSERTSOCKOP( bind( fd, ( struct sockaddr *)saddr, rp->ai_addrlen ), "Binding socket" ); + + freeaddrinfo( rp ); + + ASSERTSOCKOP( getsockname( fd, ( struct sockaddr *)saddr, &ssize ), "Getting socket name" ); + + ASSERTSOCKOP( listen( fd, 1 ), "Making socket listen" ); + + file->status = FT_STATUS_LISTENING; + + return TRUE; +} + +/* + * After setup, the transfer itself is handled entirely by this function. + * There are basically four things to handle: connect, receive, send, and error. + */ +gboolean dccs_send_proto( gpointer data, gint fd, b_input_condition cond ) +{ + dcc_file_transfer_t *df = data; + file_transfer_t *file = df->ft; + struct pollfd pfd = { .fd = fd, .events = POLLHUP|POLLERR|POLLIN|POLLOUT }; + short revents; + + if ( poll( &pfd, 1, 0 ) == -1 ) + { + imcb_log( df->ic, "poll() failed, weird!" ); + revents = 0; + }; + + revents = pfd.revents; + + if( revents & POLLERR ) + { + int sockerror; + socklen_t errlen = sizeof( sockerror ); + + if ( getsockopt( fd, SOL_SOCKET, SO_ERROR, &sockerror, &errlen ) ) + return dcc_abort( df, "getsockopt() failed, unknown socket error (weird!)" ); + + return dcc_abort( df, "Socket error: %s", strerror( sockerror ) ); + } + + if( revents & POLLHUP ) + return dcc_abort( df, "Remote end closed connection" ); + + if( ( revents & POLLIN ) && + ( file->status & FT_STATUS_LISTENING ) ) + { + struct sockaddr *clt_addr; + socklen_t ssize = sizeof( clt_addr ); + + /* Connect */ + + ASSERTSOCKOP( df->fd = accept( fd, (struct sockaddr *) &clt_addr, &ssize ), "Accepting connection" ); + + closesocket( fd ); + fd = df->fd; + file->status = FT_STATUS_TRANSFERING; + sock_make_nonblocking( fd ); + +#ifdef DCC_SEND_AHEAD + /* + * use twice the maximum segment size as a maximum for calls to send(). + */ + if( max_packet_size == 0 ) + { + unsigned int mpslen = sizeof( max_packet_size ); + if( getsockopt( fd, IPPROTO_TCP, TCP_MAXSEG, &max_packet_size, &mpslen ) ) + return dcc_abort( df, "getsockopt() failed" ); + max_packet_size *= 2; + } +#endif + /* IM protocol callback */ + + if( file->accept ) + file->accept( file ); + /* reschedule for reading on new fd */ + df->watch_in = b_input_add( fd, GAIM_INPUT_READ, dccs_send_proto, df ); + + return FALSE; + } + + if( revents & POLLIN ) + { + int bytes_received; + int ret; + + ASSERTSOCKOP( ret = recv( fd, &bytes_received, sizeof( bytes_received ), MSG_PEEK ), "Receiving" ); + + if( ret == 0 ) + return dcc_abort( df, "Remote end closed connection" ); + + if( ret < 4 ) + { + imcb_log( df->ic, "WARNING: DCC SEND: receiver sent only 2 bytes instead of 4, shouldn't happen too often!" ); + return TRUE; + } + + ASSERTSOCKOP( ret = recv( fd, &bytes_received, sizeof( bytes_received ), 0 ), "Receiving" ); + if( ret != 4 ) + return dcc_abort( df, "MSG_PEEK'ed 4, but can only dequeue %d bytes", ret ); + + bytes_received = ntohl( bytes_received ); + + /* If any of this is actually happening, the receiver should buy a new IRC client */ + + if ( bytes_received > df->bytes_sent ) + return dcc_abort( df, "Receiver magically received more bytes than sent ( %d > %d ) (BUG at receiver?)", bytes_received, df->bytes_sent ); + + if ( bytes_received < file->bytes_transferred ) + return dcc_abort( df, "Receiver lost bytes? ( has %d, had %d ) (BUG at receiver?)", bytes_received, file->bytes_transferred ); + + file->bytes_transferred = bytes_received; + + if( file->bytes_transferred >= file->file_size ) { + dcc_finish( file ); + return FALSE; + } + +#ifndef DCC_SEND_AHEAD + /* reschedule writer if neccessary */ + if( file->bytes_transferred >= df->bytes_sent && + df->watch_out == 0 && + df->queued_bytes > 0 ) { + df->watch_out = b_input_add( fd, GAIM_INPUT_WRITE, dcc_send_proto, df ); + } +#endif + return TRUE; + } + + if( revents & POLLOUT ) + { + struct dcc_buffer *dccb; + int ret; + char *msg; + + if( df->queued_bytes == 0 ) + { + /* shouldn't happen */ + imcb_log( df->ic, "WARNING: DCC SEND: write called with empty queue" ); + + df->watch_out = 0; + return FALSE; + } + + /* start where we left off */ + if( !( df->queued_buffers ) || + !( dccb = df->queued_buffers->data ) ) + return dcc_abort( df, "BUG in DCC SEND: queued data but no buffers" ); + + msg = dccb->b + df->buffer_pos; + + int msgsize = MIN( +#ifndef DCC_SEND_AHEAD + file->bytes_transferred + MAX_PACKET_SIZE - df->bytes_sent, +#else + max_packet_size, +#endif + dccb->len - df->buffer_pos ); + + if ( msgsize == 0 ) + { + df->watch_out = 0; + return FALSE; + } + + ASSERTSOCKOP( ret = send( fd, msg, msgsize, 0 ), "Sending data" ); + + if( ret == 0 ) + return dcc_abort( df, "Remote end closed connection" ); + + df->bytes_sent += ret; + df->queued_bytes -= ret; + df->buffer_pos += ret; + + if( df->buffer_pos == dccb->len ) + { + df->buffer_pos = 0; + df->queued_buffers = g_slist_remove( df->queued_buffers, dccb ); + g_free( dccb->b ); + g_free( dccb ); + } + + if( ( df->queued_bytes < DCC_QUEUE_THRESHOLD_LOW ) && file->out_of_data ) + file->out_of_data( file ); + + if( df->queued_bytes > 0 ) + { + /* Who knows how long the event loop cycle will take, + * let's just try to send some more now. */ +#ifndef DCC_SEND_AHEAD + if( df->bytes_sent < ( file->bytes_transferred + max_packet_size ) ) +#endif + return dccs_send_proto( df, fd, cond ); + } + + df->watch_out = 0; + return FALSE; + } + + /* Send buffer full, come back later */ + + return TRUE; +} + +/* + * Incoming data. Note that the buffer MUST NOT be freed by the caller! + * We don't copy the buffer but put it in our queue. + * + * */ +gboolean dccs_send_write( file_transfer_t *file, gpointer data, unsigned int data_size ) +{ + dcc_file_transfer_t *df = file->priv; + struct dcc_buffer *dccb = g_new0( struct dcc_buffer, 1 ); + + receivedchunks++; receiveddata += data_size; + + dccb->b = data; + dccb->len = data_size; + + df->queued_buffers = g_slist_append( df->queued_buffers, dccb ); + + df->queued_bytes += data_size; + + if( ( file->status & FT_STATUS_TRANSFERING ) && +#ifndef DCC_SEND_AHEAD + ( file->bytes_transferred >= df->bytes_sent ) && +#endif + ( df->watch_out == 0 ) && + ( df->queued_bytes > 0 ) ) + { + df->watch_out = b_input_add( df->fd, GAIM_INPUT_WRITE, dccs_send_proto, df ); + } + + return df->queued_bytes > DCC_QUEUE_THRESHOLD_HIGH; +} + +/* + * Cleans up after a transfer. + */ +static void dcc_close( file_transfer_t *file ) +{ + dcc_file_transfer_t *df = file->priv; + + if( file->free ) + file->free( file ); + + closesocket( df->fd ); + + if( df->watch_in ) + b_event_remove( df->watch_in ); + + if( df->watch_out ) + b_event_remove( df->watch_out ); + + if( df->queued_buffers ) + { + struct dcc_buffer *dccb; + GSList *gslist = df->queued_buffers; + + for( ; gslist ; gslist = g_slist_next( gslist ) ) + { + dccb = gslist->data; + g_free( dccb->b ); + g_free( dccb ); + } + g_slist_free( df->queued_buffers ); + } + + df->ic->irc->file_transfers = g_slist_remove( df->ic->irc->file_transfers, file ); + + g_free( df ); + g_free( file->file_name ); + g_free( file ); +} + +void dcc_finish( file_transfer_t *file ) +{ + file->status |= FT_STATUS_FINISHED; + + if( file->finished ) + file->finished( file ); + + dcc_close( file ); +} @@ -0,0 +1,125 @@ +/********************************************************************\ +* BitlBee -- An IRC to other IM-networks gateway * +* * +* Copyright 2006 Marijn Kruisselbrink and others * +* Copyright 2007 Uli Meis <a.sporto+bee@gmail.com> * +\********************************************************************/ + +/* + 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 +*/ + +/* + * DCC SEND + * + * Historically, DCC means send 1024 Bytes and wait for a 4 byte reply + * acknowledging all transferred data. This is ridiculous for two reasons. The + * first being that TCP is a stream oriented protocol that doesn't care much + * about your idea of a packet. The second reason being that TCP is a reliable + * transfer protocol with its own sophisticated ACK mechanism, making DCCs ACK + * mechanism look like a joke. For these reasons, DCCs requirements have + * (hopefully) been relaxed in most implementations and this implementation + * depends upon at least the following: The 1024 bytes need not be transferred + * at once, i.e. packets can be smaller. A second relaxation has apparently + * gotten the name "DCC SEND ahead" which basically means to not give a damn + * about those DCC ACKs and just send data as you please. This behaviour is + * enabled by default. Note that this also means that packets may be as large + * as the maximum segment size. + */ + +#ifndef _DCC_H +#define _DCC_H + +/* don't wait for acknowledgments */ +#define DCC_SEND_AHEAD + +/* This multiplier specifies how many bytes we + * can go ahead within one event loop cycle. Notice that all in all, + * we can easily be more ahead if the event loop shoots often enough. + * (or the receiver processes slow enough) + * + * Setting this value too high will cause send buffer overflows. + */ +#define DCC_SEND_AHEAD_MUL 10 + +/* + * queue thresholds for the out of data and overflow conditions + */ +#define DCC_QUEUE_THRESHOLD_LOW 2048 +#define DCC_QUEUE_THRESHOLD_HIGH 65536 + +/* only used in non-ahead mode */ +#define DCC_PACKET_SIZE 1024 + +/* stores buffers handed over by IM protocols */ +struct dcc_buffer { + char *b; + int len; +}; + +typedef struct dcc_file_transfer { + + struct im_connection *ic; + + /* + * Depending in the status of the file transfer, this is either the socket that is + * being listened on for connections, or the socket over which the file transfer is + * taking place. + */ + int fd; + + /* + * IDs returned by b_input_add for watch_ing over the above socket. + */ + gint watch_in; /* readable */ + gint watch_out; /* writable */ + + /* + * The total number of queued bytes. The following equality should always hold: + * + * queued_bytes = sum(queued_buffers.len) - buffer_pos + */ + unsigned int queued_bytes; + + /* + * A list of dcc_buffer structures. + * These are provided by the protocols directly so that no copying is neccessary. + */ + GSList *queued_buffers; + + /* + * current position within the first buffer. + * Is non-null if the whole buffer couldn't be sent at once. + */ + int buffer_pos; + + /* + * The total amount of bytes that have been sent to the irc client. + */ + size_t bytes_sent; + + /* imcb's handle */ + file_transfer_t *ft; + +} dcc_file_transfer_t; + +file_transfer_t *dccs_send_start( struct im_connection *ic, char *user_nick, char *file_name, size_t file_size ); + +void dcc_canceled( file_transfer_t *file, char *reason ); + +gboolean dccs_send_write( file_transfer_t *file, gpointer data, unsigned int data_size ); + +#endif @@ -83,6 +83,7 @@ typedef struct irc struct query *queries; struct account *accounts; + GSList *file_transfers; struct __USER *users; GHashTable *userhash; diff --git a/protocols/ft.h b/protocols/ft.h new file mode 100644 index 00000000..0ff44873 --- /dev/null +++ b/protocols/ft.h @@ -0,0 +1,153 @@ +/********************************************************************\ +* BitlBee -- An IRC to other IM-networks gateway * +* * +* Copyright 2006 Marijn Kruisselbrink and others * +\********************************************************************/ + +/* Generic file transfer header */ + +/* + 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 +*/ + +#ifndef _FT_H +#define _FT_H + +typedef enum { + FT_STATUS_LISTENING = 1, + FT_STATUS_TRANSFERING = 2, + FT_STATUS_FINISHED = 4, + FT_STATUS_CANCELED = 8 +} file_status_t; + +/* + * This structure holds all irc specific information regarding an incoming (from the point of view of + * the irc client) file transfer. New instances of this struct should only be created by calling the + * imcb_file_send_start() method, which will initialize most of the fields. The data field and the various + * methods are zero-initialized. Instances will automatically be deleted once the transfer is completed, + * canceled, or the connection to the irc client has been lost (note that also if only the irc connection + * and not the dcc connection is lost, the file transfer will still be canceled and freed). + * + * The following (poor ascii-art) diagram illustrates what methods are called for which status-changes: + * + * /-----------\ /----------\ + * -------> | LISTENING | -----------------> | CANCELED | + * \-----------/ [canceled,]free \----------/ + * | + * | accept + * V + * /------ /-------------\ /------------------------\ + * out_of_data | | TRANSFERING | -----------------> | TRANSFERING | CANCELED | + * \-----> \-------------/ [canceled,]free \------------------------/ + * | + * | finished,free + * V + * /------------------------\ + * | TRANSFERING | FINISHED | + * \------------------------/ + */ +typedef struct file_transfer { + /* + * The current status of this file transfer. + */ + file_status_t status; + + /* + * file size + */ + size_t file_size; + + /* + * Number of bytes that have been successfully transferred. + */ + size_t bytes_transferred; + + /* + * Time started. Used to calculate kb/s. + */ + time_t started; + + /* + * file name + */ + char *file_name; + + /* + * A unique local ID for this file transfer. + */ + unsigned int local_id; + + /* + * IM-protocol specific data associated with this file transfer. + */ + gpointer data; + + /* + * Private data. + */ + gpointer priv; + + /* + * If set, called after succesful connection setup. + */ + void (*accept) ( struct file_transfer *file ); + + /* + * If set, called when the transfer is canceled or finished. + * Subsequently, this structure will be freed. + * + */ + void (*free) ( struct file_transfer *file ); + + /* + * If set, called when the transfer is finished and successful. + */ + void (*finished) ( struct file_transfer *file ); + + /* + * If set, called when the transfer is canceled. + * ( canceled either by the transfer implementation or by + * a call to imcb_file_canceled ) + */ + void (*canceled) ( struct file_transfer *file, char *reason ); + + /* + * If set, called when the transfer queue is running empty and + * more data can be added. + */ + void (*out_of_data) ( struct file_transfer *file ); + +} file_transfer_t; + +/* + * This starts a file transfer from bitlbee to the user (currently via DCC). + */ +file_transfer_t *imcb_file_send_start( struct im_connection *ic, char *user_nick, char *file_name, size_t file_size ); + +/* + * This should be called by a protocol when the transfer is canceled. Note that + * the canceled() and free() callbacks given in file will be called by this function. + */ +void imcb_file_canceled( file_transfer_t *file, char *reason ); + +/* + * The given buffer is queued for transfer and MUST NOT be freed by the caller. + * When the method returns false the caller should not invoke this method again + * until out_of_data has been called. + */ +gboolean imcb_file_write( file_transfer_t *file, gpointer data, size_t data_size ); + +#endif diff --git a/protocols/jabber/Makefile b/protocols/jabber/Makefile index e042f812..47c832ae 100644 --- a/protocols/jabber/Makefile +++ b/protocols/jabber/Makefile @@ -9,7 +9,7 @@ -include ../../Makefile.settings # [SH] Program variables -objects = conference.o io.o iq.o jabber.o jabber_util.o message.o presence.o sasl.o xmltree.o +objects = conference.o io.o iq.o jabber.o jabber_util.o message.o presence.o sasl.o xmltree.o si.o stream.o CFLAGS += -Wall LFLAGS += -r diff --git a/protocols/jabber/iq.c b/protocols/jabber/iq.c index 595718fb..df0102b8 100644 --- a/protocols/jabber/iq.c +++ b/protocols/jabber/iq.c @@ -103,6 +103,9 @@ xt_status jabber_pkt_iq( struct xt_node *node, gpointer data ) XMLNS_TIME, XMLNS_CHATSTATES, XMLNS_MUC, + XMLNS_SI, + XMLNS_BYTESTREAMS, + XMLNS_FILETRANSFER, NULL }; const char **f; @@ -122,24 +125,26 @@ xt_status jabber_pkt_iq( struct xt_node *node, gpointer data ) else { xt_free_node( reply ); - reply = jabber_make_error_packet( node, "feature-not-implemented", "cancel" ); + reply = jabber_make_error_packet( node, "feature-not-implemented", "cancel", NULL ); pack = 0; } } else if( strcmp( type, "set" ) == 0 ) { - if( !( c = xt_find_node( node->children, "query" ) ) || + if( ( c = xt_find_node( node->children, "si" ) ) && + ( strcmp( xt_find_attr( c, "xmlns" ), XMLNS_SI ) == 0 ) ) + { + return jabber_si_handle_request( ic, node, c ); + } else if( !( c = xt_find_node( node->children, "query" ) ) || !( s = xt_find_attr( c, "xmlns" ) ) ) { imcb_log( ic, "WARNING: Received incomplete IQ-%s packet", type ); return XT_HANDLED; - } - + } else if( strcmp( s, XMLNS_ROSTER ) == 0 ) + { /* This is a roster push. XMPP servers send this when someone was added to (or removed from) the buddy list. AFAIK they're sent even if we added this buddy in our own session. */ - if( strcmp( s, XMLNS_ROSTER ) == 0 ) - { int bare_len = strlen( ic->acc->user ); if( ( s = xt_find_attr( node, "from" ) ) == NULL || @@ -156,14 +161,17 @@ xt_status jabber_pkt_iq( struct xt_node *node, gpointer data ) imcb_log( ic, "WARNING: %s tried to fake a roster push!", s ? s : "(unknown)" ); xt_free_node( reply ); - reply = jabber_make_error_packet( node, "not-allowed", "cancel" ); + reply = jabber_make_error_packet( node, "not-allowed", "cancel", NULL ); pack = 0; } - } - else + } else if( strcmp( s, XMLNS_BYTESTREAMS ) == 0 ) + { + /* Bytestream Request (stage 2 of file transfer) */ + return jabber_bs_request( ic, node, c ); + } else { xt_free_node( reply ); - reply = jabber_make_error_packet( node, "feature-not-implemented", "cancel" ); + reply = jabber_make_error_packet( node, "feature-not-implemented", "cancel", NULL ); pack = 0; } } diff --git a/protocols/jabber/jabber.h b/protocols/jabber/jabber.h index fc9d2fc4..0cb2b733 100644 --- a/protocols/jabber/jabber.h +++ b/protocols/jabber/jabber.h @@ -80,6 +80,8 @@ struct jabber_data char *cached_id_prefix; GHashTable *node_cache; GHashTable *buddies; + + GSList *filetransfers; }; struct jabber_away_state @@ -123,6 +125,30 @@ struct jabber_chat struct jabber_buddy *me; }; +struct jabber_transfer +{ + /* bitlbee's handle for this transfer */ + file_transfer_t *ft; + + /* the stream's private handle */ + gpointer streamhandle; + + struct im_connection *ic; + + int watch_in; + int watch_out; + + char *ini_jid; + char *tgt_jid; + char *iq_id; + char *sid; + int accepted; + + size_t bytesread, byteswritten; + int receiver_overflow; + int fd; +}; + #define JABBER_XMLCONSOLE_HANDLE "xmlconsole" #define JABBER_PORT_DEFAULT "5222" @@ -148,15 +174,21 @@ struct jabber_chat #define XMLNS_ROSTER "jabber:iq:roster" /* Some supported extensions/legacy stuff */ -#define XMLNS_AUTH "jabber:iq:auth" /* XEP-0078 */ -#define XMLNS_VERSION "jabber:iq:version" /* XEP-0092 */ -#define XMLNS_TIME "jabber:iq:time" /* XEP-0090 */ -#define XMLNS_VCARD "vcard-temp" /* XEP-0054 */ -#define XMLNS_DELAY "jabber:x:delay" /* XEP-0091 */ -#define XMLNS_CHATSTATES "http://jabber.org/protocol/chatstates" /* 0085 */ -#define XMLNS_DISCOVER "http://jabber.org/protocol/disco#info" /* 0030 */ -#define XMLNS_MUC "http://jabber.org/protocol/muc" /* XEP-0045 */ -#define XMLNS_MUC_USER "http://jabber.org/protocol/muc#user"/* XEP-0045 */ +#define XMLNS_AUTH "jabber:iq:auth" /* XEP-0078 */ +#define XMLNS_VERSION "jabber:iq:version" /* XEP-0092 */ +#define XMLNS_TIME "jabber:iq:time" /* XEP-0090 */ +#define XMLNS_VCARD "vcard-temp" /* XEP-0054 */ +#define XMLNS_DELAY "jabber:x:delay" /* XEP-0091 */ +#define XMLNS_XDATA "jabber:x:data" /* XEP-0004 */ +#define XMLNS_CHATSTATES "http://jabber.org/protocol/chatstates" /* XEP-0085 */ +#define XMLNS_DISCOVER "http://jabber.org/protocol/disco#info" /* XEP-0030 */ +#define XMLNS_MUC "http://jabber.org/protocol/muc" /* XEP-0045 */ +#define XMLNS_MUC_USER "http://jabber.org/protocol/muc#user" /* XEP-0045 */ +#define XMLNS_FEATURE "http://jabber.org/protocol/feature-neg" /* XEP-0020 */ +#define XMLNS_SI "http://jabber.org/protocol/si" /* XEP-0095 */ +#define XMLNS_FILETRANSFER "http://jabber.org/protocol/si/profile/file-transfer" /* XEP-0096 */ +#define XMLNS_BYTESTREAMS "http://jabber.org/protocol/bytestreams" /* XEP-0065 */ +#define XMLNS_IBB "http://jabber.org/protocol/ibb" /* XEP-0047 */ /* iq.c */ xt_status jabber_pkt_iq( struct xt_node *node, gpointer data ); @@ -167,6 +199,12 @@ int jabber_get_vcard( struct im_connection *ic, char *bare_jid ); int jabber_add_to_roster( struct im_connection *ic, char *handle, char *name ); int jabber_remove_from_roster( struct im_connection *ic, char *handle ); +/* si.c */ +int jabber_si_handle_request( struct im_connection *ic, struct xt_node *node, struct xt_node *sinode); + +/* stream.c */ +int jabber_bs_request( struct im_connection *ic, struct xt_node *node, struct xt_node *qnode); + /* message.c */ xt_status jabber_pkt_message( struct xt_node *node, gpointer data ); @@ -179,7 +217,7 @@ int presence_send_request( struct im_connection *ic, char *handle, char *request char *set_eval_priority( set_t *set, char *value ); char *set_eval_tls( set_t *set, char *value ); struct xt_node *jabber_make_packet( char *name, char *type, char *to, struct xt_node *children ); -struct xt_node *jabber_make_error_packet( struct xt_node *orig, char *err_cond, char *err_type ); +struct xt_node *jabber_make_error_packet( struct xt_node *orig, char *err_cond, char *err_type, char *err_code ); void jabber_cache_add( struct im_connection *ic, struct xt_node *node, jabber_cache_event func ); struct xt_node *jabber_cache_get( struct im_connection *ic, char *id ); void jabber_cache_entry_free( gpointer entry ); diff --git a/protocols/jabber/jabber_util.c b/protocols/jabber/jabber_util.c index 43b91fe3..0c5b813e 100644 --- a/protocols/jabber/jabber_util.c +++ b/protocols/jabber/jabber_util.c @@ -96,7 +96,7 @@ struct xt_node *jabber_make_packet( char *name, char *type, char *to, struct xt_ return node; } -struct xt_node *jabber_make_error_packet( struct xt_node *orig, char *err_cond, char *err_type ) +struct xt_node *jabber_make_error_packet( struct xt_node *orig, char *err_cond, char *err_type, char *err_code ) { struct xt_node *node, *c; char *to; @@ -109,6 +109,10 @@ struct xt_node *jabber_make_error_packet( struct xt_node *orig, char *err_cond, c = xt_new_node( "error", NULL, c ); xt_add_attr( c, "type", err_type ); + /* Add the error code, if present */ + if (err_code) + xt_add_attr( c, "code", err_code ); + /* To make the actual error packet, we copy the original packet and add our <error>/type="error" tag. Including the original packet is recommended, so let's just do it. */ diff --git a/protocols/jabber/si.c b/protocols/jabber/si.c new file mode 100644 index 00000000..d16f723a --- /dev/null +++ b/protocols/jabber/si.c @@ -0,0 +1,246 @@ +/***************************************************************************\ +* * +* BitlBee - An IRC to IM gateway * +* Jabber module - SI packets * +* * +* Copyright 2007 Uli Meis <a.sporto+bee@gmail.com> * +* * +* 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 02110-1301 USA. * +* * +\***************************************************************************/ + +#include "jabber.h" +#include "sha1.h" + +void jabber_si_answer_request( file_transfer_t *ft ); + +/* imcb callback */ +void jabber_si_free_transfer( file_transfer_t *ft) +{ + struct jabber_transfer *tf = ft->data; + struct jabber_data *jd = tf->ic->proto_data; + + if ( tf->watch_in ) + b_event_remove( tf->watch_in ); + + jd->filetransfers = g_slist_remove( jd->filetransfers, tf ); + + if( tf->fd ) + { + close( tf->fd ); + tf->fd = 0; + } + + g_free( tf->ini_jid ); + g_free( tf->tgt_jid ); + g_free( tf->iq_id ); + g_free( tf->sid ); +} + +/* imcb callback */ +void jabber_si_finished( file_transfer_t *ft ) +{ + struct jabber_transfer *tf = ft->data; + + imcb_log( tf->ic, "File %s transferred successfully!" , ft->file_name ); +} + +/* imcb callback */ +void jabber_si_canceled( file_transfer_t *ft, char *reason ) +{ + struct jabber_transfer *tf = ft->data; + struct xt_node *reply, *iqnode; + + if( tf->accepted ) + return; + + iqnode = jabber_make_packet( "iq", "error", tf->ini_jid, NULL ); + xt_add_attr( iqnode, "id", tf->iq_id ); + reply = jabber_make_error_packet( iqnode, "forbidden", "cancel", "403" ); + xt_free_node( iqnode ); + + if( !jabber_write_packet( tf->ic, reply ) ) + imcb_log( tf->ic, "WARNING: Error generating reply to file transfer request" ); + xt_free_node( reply ); + +} + +/* + * First function that gets called when a file transfer request comes in. + * A lot to parse. + * + * We choose a stream type from the options given by the initiator. + * Then we wait for imcb to call the accept or cancel callbacks. + */ +int jabber_si_handle_request( struct im_connection *ic, struct xt_node *node, struct xt_node *sinode) +{ + struct xt_node *c, *d, *reply; + char *sid, *ini_jid, *tgt_jid, *iq_id, *s, *ext_jid; + struct jabber_buddy *bud; + int requestok = FALSE; + char *name; + size_t size; + struct jabber_transfer *tf; + struct jabber_data *jd = ic->proto_data; + file_transfer_t *ft; + + /* All this means we expect something like this: ( I think ) + * <iq from=... to=... id=...> + * <si id=id xmlns=si profile=ft> + * <file xmlns=ft/> + * <feature xmlns=feature> + * <x xmlns=xdata type=submit> + * <field var=stream-method> + * + */ + if( !( ini_jid = xt_find_attr( node, "from" ) ) || + !( tgt_jid = xt_find_attr( node, "to" ) ) || + !( iq_id = xt_find_attr( node, "id" ) ) || + !( sid = xt_find_attr( sinode, "id" ) ) || + !( strcmp( xt_find_attr( sinode, "profile" ), XMLNS_FILETRANSFER ) == 0 ) || + !( d = xt_find_node( sinode->children, "file" ) ) || + !( strcmp( xt_find_attr( d, "xmlns" ), XMLNS_FILETRANSFER ) == 0 ) || + !( name = xt_find_attr( d, "name" ) ) || + !( size = (size_t) atoll( xt_find_attr( d, "size" ) ) ) || + !( d = xt_find_node( sinode->children, "feature" ) ) || + !( strcmp( xt_find_attr( d, "xmlns" ), XMLNS_FEATURE ) == 0 ) || + !( d = xt_find_node( d->children, "x" ) ) || + !( strcmp( xt_find_attr( d, "xmlns" ), XMLNS_XDATA ) == 0 ) || + !( strcmp( xt_find_attr( d, "type" ), "form" ) == 0 ) || + !( d = xt_find_node( d->children, "field" ) ) || + !( strcmp( xt_find_attr( d, "var" ), "stream-method" ) == 0 ) ) + { + imcb_log( ic, "WARNING: Received incomplete Stream Initiation request" ); + } else + { + /* Check if we support one of the options */ + + c = d->children; + while( ( c = xt_find_node( c, "option" ) ) ) + if( ( d = xt_find_node( c->children, "value" ) ) && + ( strcmp( d->text, XMLNS_BYTESTREAMS ) == 0 ) ) + { + requestok = TRUE; + break; + } + } + + if ( requestok ) + { + /* Figure out who the transfer should come frome... */ + + if( ( s = strchr( ini_jid, '/' ) ) ) + { + if( ( bud = jabber_buddy_by_jid( ic, ini_jid, GET_BUDDY_EXACT ) ) ) + { + bud->last_act = time( NULL ); + ext_jid = bud->ext_jid ? : bud->bare_jid; + } + else + *s = 0; /* We need to generate a bare JID now. */ + } + + if( !( ft = imcb_file_send_start( ic, ext_jid, name, size ) ) ) + { + imcb_log( ic, "WARNING: Error handling transfer request from %s", ini_jid); + requestok = FALSE; + } + + *s = '/'; + } else + imcb_log( ic, "WARNING: Unsupported file transfer request from %s", ini_jid); + + if ( !requestok ) + { + reply = jabber_make_error_packet( node, "item-not-found", "cancel", NULL ); + if (!jabber_write_packet( ic, reply )) + imcb_log( ic, "WARNING: Error generating reply to file transfer request" ); + xt_free_node( reply ); + return XT_HANDLED; + } + + /* Request is fine. */ + + imcb_log( ic, "File transfer request from %s for %s (%zd kb). ", xt_find_attr( node, "from" ), name, size/1024 ); + + imcb_log( ic, "Accept the DCC transfer if you'd like the file. If you don't, issue the 'transfers reject' command."); + + tf = g_new0( struct jabber_transfer, 1 ); + + tf->ini_jid = g_strdup( ini_jid ); + tf->tgt_jid = g_strdup( tgt_jid ); + tf->iq_id = g_strdup( iq_id ); + tf->sid = g_strdup( sid ); + tf->ic = ic; + tf->ft = ft; + tf->ft->data = tf; + tf->ft->accept = jabber_si_answer_request; + tf->ft->free = jabber_si_free_transfer; + tf->ft->finished = jabber_si_finished; + tf->ft->canceled = jabber_si_canceled; + + jd->filetransfers = g_slist_prepend( jd->filetransfers, tf ); + + return XT_HANDLED; +} + +/* + * imcb called the accept callback which probably means that the user accepted this file transfer. + * We send our response to the initiator. + * In the next step, the initiator will send us a request for the given stream type. + * (currently that can only be a SOCKS5 bytestream) + */ +void jabber_si_answer_request( file_transfer_t *ft ) { + struct jabber_transfer *tf = ft->data; + struct xt_node *node, *sinode, *reply; + + /* generate response, start with the SI tag */ + sinode = xt_new_node( "si", NULL, NULL ); + xt_add_attr( sinode, "xmlns", XMLNS_SI ); + xt_add_attr( sinode, "profile", XMLNS_FILETRANSFER ); + xt_add_attr( sinode, "id", tf->sid ); + + /* now the file tag */ + node = xt_new_node( "file", NULL, NULL ); + xt_add_attr( node, "xmlns", XMLNS_FILETRANSFER ); + + xt_add_child( sinode, node ); + + /* and finally the feature tag */ + node = xt_new_node( "field", NULL, NULL ); + xt_add_attr( node, "var", "stream-method" ); + xt_add_attr( node, "type", "list-single" ); + + /* Currently all we can do. One could also implement in-band (IBB) */ + xt_add_child( node, xt_new_node( "value", XMLNS_BYTESTREAMS, NULL ) ); + + node = xt_new_node( "x", NULL, node ); + xt_add_attr( node, "xmlns", XMLNS_XDATA ); + xt_add_attr( node, "type", "submit" ); + + node = xt_new_node( "feature", NULL, node ); + xt_add_attr( node, "xmlns", XMLNS_FEATURE ); + + xt_add_child( sinode, node ); + + reply = jabber_make_packet( "iq", "result", tf->ini_jid, sinode ); + xt_add_attr( reply, "id", tf->iq_id ); + + if( !jabber_write_packet( tf->ic, reply ) ) + imcb_log( tf->ic, "WARNING: Error generating reply to file transfer request" ); + else + tf->accepted = TRUE; + xt_free_node( reply ); +} diff --git a/protocols/jabber/stream.c b/protocols/jabber/stream.c new file mode 100644 index 00000000..c88a72fd --- /dev/null +++ b/protocols/jabber/stream.c @@ -0,0 +1,593 @@ +/***************************************************************************\ +* * +* BitlBee - An IRC to IM gateway * +* Jabber module - stream handling * +* * +* Copyright 2007 Uli Meis <a.sporto+bee@gmail.com> * +* * +* 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 02110-1301 USA. * +* * +\***************************************************************************/ + +#include "jabber.h" +#include "sha1.h" +#include <poll.h> + +/* Some structs for the SOCKS5 handshake */ + +struct bs_handshake_data { + + struct jabber_transfer *tf; + + /* <query> element and <streamhost> elements */ + struct xt_node *qnode, *shnode; + + enum + { + BS_PHASE_CONNECT, + BS_PHASE_CONNECTED, + BS_PHASE_REQUEST, + BS_PHASE_REPLY, + BS_PHASE_REPLY_HAVE_LEN + } phase; + + /* SHA1( SID + Initiator JID + Target JID) */ + char *pseudoadr; + + void (*parentfree) ( file_transfer_t *ft ); + + gint connect_timeout; +}; + +struct socks5_hdr +{ + unsigned char ver; + union + { + unsigned char cmd; + unsigned char rep; + } cmdrep; + unsigned char rsv; + unsigned char atyp; +}; + +struct socks5_message +{ + struct socks5_hdr hdr; + unsigned char addrlen; + unsigned char address[64]; +}; + +/* connect() timeout in seconds. */ +#define JABBER_BS_CONTIMEOUT 15 + +/* shouldn't matter if it's mostly too much, kernel's smart about that + * and will only reserve some address space */ +#define JABBER_BS_BUFSIZE 65536 + +gboolean jabber_bs_handshake( gpointer data, gint fd, b_input_condition cond ); + +gboolean jabber_bs_handshake_abort( struct bs_handshake_data *bhd, char *format, ... ); + +void jabber_bs_answer_request( struct bs_handshake_data *bhd ); + +gboolean jabber_bs_read( gpointer data, gint fd, b_input_condition cond ); + +void jabber_bs_out_of_data( file_transfer_t *ft ); + +void jabber_bs_canceled( file_transfer_t *ft , char *reason ); + + +void jabber_bs_free_transfer( file_transfer_t *ft) { + struct jabber_transfer *tf = ft->data; + struct bs_handshake_data *bhd = tf->streamhandle; + void (*parentfree) ( file_transfer_t *ft ); + + parentfree = bhd->parentfree; + + if ( tf->watch_in ) + b_event_remove( tf->watch_in ); + + if( tf->watch_out ) + b_event_remove( tf->watch_out ); + + g_free( bhd->pseudoadr ); + xt_free_node( bhd->qnode ); + g_free( bhd ); + + parentfree( ft ); +} + +/* + * Parses an incoming bytestream request and calls jabber_bs_handshake on success. + */ +int jabber_bs_request( struct im_connection *ic, struct xt_node *node, struct xt_node *qnode) +{ + char *sid, *ini_jid, *tgt_jid, *mode, *iq_id; + struct jabber_data *jd = ic->proto_data; + struct jabber_transfer *tf = NULL; + GSList *tflist; + struct bs_handshake_data *bhd; + + sha1_state_t sha; + char hash_hex[41]; + unsigned char hash[20]; + int i; + + if( !(iq_id = xt_find_attr( node, "id" ) ) || + !(ini_jid = xt_find_attr( node, "from" ) ) || + !(tgt_jid = xt_find_attr( node, "to" ) ) || + !(sid = xt_find_attr( qnode, "sid" ) ) ) + { + imcb_log( ic, "WARNING: Received incomplete SI bytestream request"); + return XT_HANDLED; + } + + if( ( mode = xt_find_attr( qnode, "mode" ) ) && + ( strcmp( mode, "tcp" ) != 0 ) ) + { + imcb_log( ic, "WARNING: Received SI Request for unsupported bytestream mode %s", xt_find_attr( qnode, "mode" ) ); + return XT_HANDLED; + } + + /* Let's see if we can find out what this bytestream should be for... */ + + for( tflist = jd->filetransfers ; tflist; tflist = g_slist_next(tflist) ) + { + struct jabber_transfer *tft = tflist->data; + if( ( strcmp( tft->sid, sid ) == 0 ) && + ( strcmp( tft->ini_jid, ini_jid ) == 0 ) && + ( strcmp( tft->tgt_jid, tgt_jid ) == 0 ) ) + { + tf = tft; + break; + } + } + + if (!tf) + { + imcb_log( ic, "WARNING: Received bytestream request from %s that doesn't match an SI request", ini_jid ); + return XT_HANDLED; + } + + /* iq_id and canceled can be reused since SI is done */ + g_free( tf->iq_id ); + tf->iq_id = g_strdup( iq_id ); + + tf->ft->canceled = jabber_bs_canceled; + + /* SHA1( SID + Initiator JID + Target JID ) is given to the streamhost which it will match against the initiator's value */ + sha1_init( &sha ); + sha1_append( &sha, (unsigned char*) sid, strlen( sid ) ); + sha1_append( &sha, (unsigned char*) ini_jid, strlen( ini_jid ) ); + sha1_append( &sha, (unsigned char*) tgt_jid, strlen( tgt_jid ) ); + sha1_finish( &sha, hash ); + + for( i = 0; i < 20; i ++ ) + sprintf( hash_hex + i * 2, "%02x", hash[i] ); + + bhd = g_new0( struct bs_handshake_data, 1 ); + bhd->tf = tf; + bhd->qnode = xt_dup( qnode ); + bhd->shnode = bhd->qnode->children; + bhd->phase = BS_PHASE_CONNECT; + bhd->pseudoadr = g_strdup( hash_hex ); + tf->streamhandle = bhd; + bhd->parentfree = tf->ft->free; + tf->ft->free = jabber_bs_free_transfer; + + jabber_bs_handshake( bhd, 0, 0 ); + + return XT_HANDLED; +} + +/* + * This function is scheduled in bs_handshake via b_timeout_add after a (non-blocking) connect(). + */ +gboolean jabber_bs_connect_timeout( gpointer data, gint fd, b_input_condition cond ) +{ + struct bs_handshake_data *bhd = data; + + bhd->connect_timeout = 0; + + jabber_bs_handshake_abort( bhd, "no connection after %d seconds", JABBER_BS_CONTIMEOUT ); + + return FALSE; +} + +/* + * This is what a protocol handshake can look like in cooperative multitasking :) + * Might be confusing at first because it's called from different places and is recursing. + * (places being the event thread, bs_request, bs_handshake_abort, and itself) + * + * All in all, it turned out quite nice :) + */ +gboolean jabber_bs_handshake( gpointer data, gint fd, b_input_condition cond ) +{ + +/* very useful */ +#define ASSERTSOCKOP(op, msg) \ + if( (op) == -1 ) \ + return jabber_bs_handshake_abort( bhd , msg ": %s", strerror( errno ) ); + + struct bs_handshake_data *bhd = data; + struct pollfd pfd = { .fd = fd, .events = POLLHUP|POLLERR }; + short revents; + + if ( bhd->connect_timeout ) + { + b_event_remove( bhd->connect_timeout ); + bhd->connect_timeout = 0; + } + + + /* we need the real io condition */ + if ( poll( &pfd, 1, 0 ) == -1 ) + { + imcb_log( bhd->tf->ic, "poll() failed, weird!" ); + revents = 0; + }; + + revents = pfd.revents; + + if( revents & POLLERR ) + { + int sockerror; + socklen_t errlen = sizeof( sockerror ); + + if ( getsockopt( fd, SOL_SOCKET, SO_ERROR, &sockerror, &errlen ) ) + return jabber_bs_handshake_abort( bhd, "getsockopt() failed, unknown socket error during SOCKS5 handshake (weird!)" ); + + if ( bhd->phase == BS_PHASE_CONNECTED ) + return jabber_bs_handshake_abort( bhd, "connect() failed: %s", strerror( sockerror ) ); + + return jabber_bs_handshake_abort( bhd, "Socket error during SOCKS5 handshake(weird!): %s", strerror( sockerror ) ); + } + + if( revents & POLLHUP ) + return jabber_bs_handshake_abort( bhd, "Remote end closed connection" ); + + + switch( bhd->phase ) + { + case BS_PHASE_CONNECT: + { + struct xt_node *c; + char *host, *port; + struct addrinfo hints, *rp; + + if( ( c = bhd->shnode = xt_find_node( bhd->shnode, "streamhost" ) ) && + ( port = xt_find_attr( c, "port" ) ) && + ( host = xt_find_attr( c, "host" ) ) && + xt_find_attr( c, "jid" ) ) + { + memset( &hints, 0, sizeof( struct addrinfo ) ); + hints.ai_socktype = SOCK_STREAM; + + if ( getaddrinfo( host, port, &hints, &rp ) != 0 ) + return jabber_bs_handshake_abort( bhd, "getaddrinfo() failed: %s", strerror( errno ) ); + + ASSERTSOCKOP( bhd->tf->fd = fd = socket( rp->ai_family, rp->ai_socktype, 0 ), "Opening socket" ); + + sock_make_nonblocking( fd ); + + imcb_log( bhd->tf->ic, "Transferring file %s: Connecting to streamhost %s:%s", bhd->tf->ft->file_name, host, port ); + + if( ( connect( fd, rp->ai_addr, rp->ai_addrlen ) == -1 ) && + ( errno != EINPROGRESS ) ) + return jabber_bs_handshake_abort( bhd , "connect() failed: %s", strerror( errno ) ); + + freeaddrinfo( rp ); + + bhd->phase = BS_PHASE_CONNECTED; + + bhd->tf->watch_out = b_input_add( fd, GAIM_INPUT_WRITE, jabber_bs_handshake, bhd ); + + /* since it takes forever(3mins?) till connect() fails on itself we schedule a timeout */ + bhd->connect_timeout = b_timeout_add( JABBER_BS_CONTIMEOUT * 1000, jabber_bs_connect_timeout, bhd ); + + bhd->tf->watch_in = 0; + return FALSE; + } else + return jabber_bs_handshake_abort( bhd, c ? "incomplete streamhost entry: host=%s port=%s jid=%s" : NULL, + host, port, xt_find_attr( c, "jid" ) ); + } + case BS_PHASE_CONNECTED: + { + struct { + unsigned char ver; + unsigned char nmethods; + unsigned char method; + } socks5_hello = { + .ver = 5, + .nmethods = 1, + .method = 0x00 /* no auth */ + /* one could also implement username/password. If you know + * a jabber client or proxy that actually does it, tell me. + */ + }; + + ASSERTSOCKOP( send( fd, &socks5_hello, sizeof( socks5_hello ) , 0 ), "Sending auth request" ); + + bhd->phase = BS_PHASE_REQUEST; + + bhd->tf->watch_in = b_input_add( fd, GAIM_INPUT_READ, jabber_bs_handshake, bhd ); + + bhd->tf->watch_out = 0; + return FALSE; + } + case BS_PHASE_REQUEST: + { + struct socks5_message socks5_connect = + { + .hdr = + { + .ver = 5, + .cmdrep.cmd = 0x01, + .rsv = 0, + .atyp = 0x03 + }, + .addrlen = strlen( bhd->pseudoadr ) + }; + int ret; + char buf[2]; + + /* If someone's trying to be funny and sends only one byte at a time we'll fail :) */ + ASSERTSOCKOP( ret = recv( fd, buf, 2, 0 ) , "Receiving auth reply" ); + + if( !( ret == 2 ) || + !( buf[0] == 5 ) || + !( buf[1] == 0 ) ) + return jabber_bs_handshake_abort( bhd, "Auth not accepted by streamhost (reply: len=%d, ver=%d, status=%d)", + ret, buf[0], buf[1] ); + + /* copy hash into connect message */ + memcpy( socks5_connect.address, bhd->pseudoadr, socks5_connect.addrlen ); + + /* after the address comes the port, which is always 0 */ + memset( socks5_connect.address + socks5_connect.addrlen, 0, sizeof( in_port_t ) ); + + ASSERTSOCKOP( send( fd, &socks5_connect, sizeof( struct socks5_hdr ) + 1 + socks5_connect.addrlen + sizeof( in_port_t ), 0 ) , "Sending SOCKS5 Connect" ); + + bhd->phase = BS_PHASE_REPLY; + + return TRUE; + } + case BS_PHASE_REPLY: + case BS_PHASE_REPLY_HAVE_LEN: + { + /* we have to wait till we have the address length, then we know how much data is left + * (not that we'd actually care about that data, but we need to eat it all up anyway) + */ + struct socks5_message socks5_reply; + int ret; + int expectedbytes = + sizeof( struct socks5_hdr ) + 1 + + ( bhd->phase == BS_PHASE_REPLY_HAVE_LEN ? socks5_reply.addrlen + sizeof( in_port_t ) : 0 ); + + /* notice the peek, we're doing this till enough is there */ + ASSERTSOCKOP( ret = recv( fd, &socks5_reply, expectedbytes, MSG_PEEK ) , "Peeking for SOCKS5 CONNECT reply" ); + + if ( ret == 0 ) + return jabber_bs_handshake_abort( bhd , "peer has shutdown connection" ); + + /* come again */ + if ( ret < expectedbytes ) + return TRUE; + + if ( bhd->phase == BS_PHASE_REPLY ) + { + if( !( socks5_reply.hdr.ver == 5 ) || + !( socks5_reply.hdr.cmdrep.rep == 0 ) || + !( socks5_reply.hdr.atyp == 3 ) || + !( socks5_reply.addrlen <= 62 ) ) /* should also be 40, but who cares as long as all fits in the buffer... */ + return jabber_bs_handshake_abort( bhd, "SOCKS5 CONNECT failed (reply: ver=%d, rep=%d, atyp=%d, addrlen=%d", + socks5_reply.hdr.ver, + socks5_reply.hdr.cmdrep.rep, + socks5_reply.hdr.atyp, + socks5_reply.addrlen); + + /* and again for the rest */ + bhd->phase = BS_PHASE_REPLY_HAVE_LEN; + + /* since it's very likely that the rest is there as well, + * let's not wait for the event loop to call us again */ + return jabber_bs_handshake( bhd , fd, 0 ); + } + + /* got it all, remove it from the queue */ + ASSERTSOCKOP( ret = recv( fd, &socks5_reply, expectedbytes, 0 ) , "Dequeueing MSG_PEEK'ed data after SOCKS5 CONNECT" ); + + /* this shouldn't happen */ + if ( ret < expectedbytes ) + return jabber_bs_handshake_abort( bhd, "internal error, couldn't dequeue MSG_PEEK'ed data after SOCKS5 CONNECT" ); + + /* we're actually done now... */ + + jabber_bs_answer_request( bhd ); + + bhd->tf->watch_in = 0; + return FALSE; + } + default: + /* BUG */ + imcb_log( bhd->tf->ic, "BUG in file transfer code: undefined handshake phase" ); + + bhd->tf->watch_in = 0; + return FALSE; + } +#undef ASSERTSOCKOP +#undef JABBER_BS_ERR_CONDS +} + +/* + * If the handshake failed we can try the next streamhost, if there is one. + * An intelligent sender would probably specify himself as the first streamhost and + * a proxy as the second (Kopete is an example here). That way, a (potentially) + * slow proxy is only used if neccessary. + */ +gboolean jabber_bs_handshake_abort( struct bs_handshake_data *bhd, char *format, ... ) +{ + struct jabber_transfer *tf = bhd->tf; + struct xt_node *reply, *iqnode; + + if( bhd->shnode ) + { + if( 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_log( tf->ic, "Transferring file %s: connection to streamhost %s:%s failed (%s)", + tf->ft->file_name, + xt_find_attr( bhd->shnode, "host" ), + xt_find_attr( bhd->shnode, "port" ), + error ); + } + + /* Alright, this streamhost failed, let's try the next... */ + bhd->phase = BS_PHASE_CONNECT; + bhd->shnode = bhd->shnode->next; + + /* the if is not neccessary but saves us one recursion */ + if( bhd->shnode ) + return jabber_bs_handshake( bhd, 0, 0 ); + } + + /* out of stream hosts */ + + iqnode = jabber_make_packet( "iq", "result", tf->ini_jid, NULL ); + reply = jabber_make_error_packet( iqnode, "item-not-found", "cancel" , "404" ); + xt_free_node( iqnode ); + + xt_add_attr( reply, "id", tf->iq_id ); + + if( !jabber_write_packet( tf->ic, reply ) ) + imcb_log( tf->ic, "WARNING: Error transmitting bytestream response" ); + xt_free_node( reply ); + + imcb_file_canceled( tf->ft, "couldn't connect to any streamhosts" ); + + bhd->tf->watch_in = 0; + return FALSE; +} + +/* + * After the SOCKS5 handshake succeeds we need to inform the initiator which streamhost we chose. + * If he is the streamhost himself, he might already know that. However, if it's a proxy, + * the initiator will have to make a connection himself. + */ +void jabber_bs_answer_request( struct bs_handshake_data *bhd ) +{ + struct jabber_transfer *tf = bhd->tf; + struct xt_node *reply; + + imcb_log( tf->ic, "Transferring file %s: established SOCKS5 connection to %s:%s", + tf->ft->file_name, + xt_find_attr( bhd->shnode, "host" ), + xt_find_attr( bhd->shnode, "port" ) ); + + tf->ft->data = tf; + tf->ft->started = time( NULL ); + tf->watch_in = b_input_add( tf->fd, GAIM_INPUT_READ, jabber_bs_read, tf ); + tf->ft->out_of_data = jabber_bs_out_of_data; + + reply = xt_new_node( "streamhost-used", NULL, NULL ); + xt_add_attr( reply, "jid", xt_find_attr( bhd->shnode, "jid" ) ); + + reply = xt_new_node( "query", NULL, reply ); + xt_add_attr( reply, "xmlns", XMLNS_BYTESTREAMS ); + + reply = jabber_make_packet( "iq", "result", tf->ini_jid, reply ); + + xt_add_attr( reply, "id", tf->iq_id ); + + if( !jabber_write_packet( tf->ic, reply ) ) + imcb_file_canceled( tf->ft, "Error transmitting bytestream response" ); + xt_free_node( reply ); +} + +/* Reads till it is unscheduled or the receiver signifies an overflow. */ +gboolean jabber_bs_read( gpointer data, gint fd, b_input_condition cond ) +{ + int ret; + struct jabber_transfer *tf = data; + char *buffer = g_malloc( JABBER_BS_BUFSIZE ); + + if (tf->receiver_overflow) + { + if( tf->watch_in ) + { + /* should never happen, BUG */ + imcb_file_canceled( tf->ft, "Bug in jabber file transfer code: read while overflow is true. Please report" ); + return FALSE; + } + } + + ret = recv( fd, buffer, JABBER_BS_BUFSIZE, 0 ); + + if( ret == -1 ) + { + /* shouldn't actually happen */ + if( errno == EAGAIN ) + return TRUE; + + imcb_file_canceled( tf->ft, "Error reading tcp socket" ); /* , strerror( errnum ) */ + + return FALSE; + } + + /* that should be all */ + if( ret == 0 ) + return FALSE; + + tf->bytesread += ret; + + buffer = g_realloc( buffer, ret ); + + if ( ( tf->receiver_overflow = imcb_file_write( tf->ft, buffer, ret ) ) ) + { + /* wait for imcb to run out of data */ + tf->watch_in = 0; + return FALSE; + } + + + return TRUE; +} + +/* imcb callback that is invoked when it runs out of data. + * We reschedule jabber_bs_read here if neccessary. */ +void jabber_bs_out_of_data( file_transfer_t *ft ) +{ + struct jabber_transfer *tf = ft->data; + + tf->receiver_overflow = FALSE; + + if ( !tf->watch_in ) + tf->watch_in = b_input_add( tf->fd, GAIM_INPUT_READ, jabber_bs_read, tf ); +} + +/* Bad luck */ +void jabber_bs_canceled( file_transfer_t *ft , char *reason ) +{ + struct jabber_transfer *tf = ft->data; + + imcb_log( tf->ic, "File transfer aborted: %s", reason ); +} diff --git a/protocols/nogaim.h b/protocols/nogaim.h index 0e890464..8651754a 100644 --- a/protocols/nogaim.h +++ b/protocols/nogaim.h @@ -42,6 +42,7 @@ #include "account.h" #include "proxy.h" #include "md5.h" +#include "ft.h" #define BUF_LEN MSG_LEN #define BUF_LONG ( BUF_LEN * 2 ) diff --git a/root_commands.c b/root_commands.c index 642f5374..2da77519 100644 --- a/root_commands.c +++ b/root_commands.c @@ -974,6 +974,71 @@ static void cmd_join_chat( irc_t *irc, char **cmd ) } } +static void cmd_transfers( irc_t *irc, char **cmd ) +{ + GSList *files = irc->file_transfers; + enum { LIST, REJECT, CANCEL }; + int subcmd = LIST; + int fid; + + if( !files ) + { + irc_usermsg( irc, "No pending transfers" ); + return; + } + + if( cmd[1] && + ( strcmp( cmd[1], "reject" ) == 0 ) ) + { + subcmd = REJECT; + } + else if( cmd[1] && + ( strcmp( cmd[1], "cancel" ) == 0 ) && + cmd[2] && + ( fid = atoi( cmd[2] ) ) ) + { + subcmd = CANCEL; + } + + for( ; files; files = g_slist_next( files ) ) + { + file_transfer_t *file = files->data; + + switch( subcmd ) { + case LIST: + if ( file->status == FT_STATUS_LISTENING ) + irc_usermsg( irc, + "Pending file(id %d): %s (Listening...)", file->local_id, file->file_name); + else + { + int kb_per_s = 0; + time_t diff = time( NULL ) - file->started; + if ( ( file->started > 0 ) && ( file->bytes_transferred > 0 ) ) + kb_per_s = file->bytes_transferred / 1024 / diff; + + irc_usermsg( irc, + "Pending file(id %d): %s (%10zd/%zd kb, %d kb/s)", file->local_id, file->file_name, + file->bytes_transferred/1024, file->file_size/1024, kb_per_s); + } + break; + case REJECT: + if( file->status == FT_STATUS_LISTENING ) + { + irc_usermsg( irc, "Rejecting file transfer for %s", file->file_name ); + imcb_file_canceled( file, "Denied by user" ); + } + break; + case CANCEL: + if( file->local_id == fid ) + { + irc_usermsg( irc, "Canceling file transfer for %s", file->file_name ); + imcb_file_canceled( file, "Canceled by user" ); + } + break; + } + } +} + const command_t commands[] = { { "help", 0, cmd_help, 0 }, { "identify", 1, cmd_identify, 0 }, @@ -994,5 +1059,6 @@ const command_t commands[] = { { "nick", 1, cmd_nick, 0 }, { "qlist", 0, cmd_qlist, 0 }, { "join_chat", 2, cmd_join_chat, 0 }, + { "transfers", 0, cmd_transfers, 0 }, { NULL } }; |