Mercurial > pidgin
changeset 25738:26c85b154f3a
propagate from branch 'im.pidgin.pidgin' (head 60eb3945122f9f81bed98851da3567b10baf9450)
to branch 'im.pidgin.cpw.malu.xmpp.ibb_ft' (head d384cfa5b3ab9b8a8b81063db6b8ebcf9e35624f)
author | Marcus Lundblad <ml@update.uu.se> |
---|---|
date | Wed, 22 Oct 2008 17:47:22 +0000 |
parents | ce1086a69ef2 (current diff) e4f8641cc62c (diff) |
children | 45dbea660309 |
files | |
diffstat | 9 files changed, 1085 insertions(+), 56 deletions(-) [+] |
line wrap: on
line diff
--- a/libpurple/protocols/jabber/Makefile.am Wed Oct 22 15:23:17 2008 +0000 +++ b/libpurple/protocols/jabber/Makefile.am Wed Oct 22 17:47:22 2008 +0000 @@ -17,6 +17,8 @@ disco.h \ google.c \ google.h \ + ibb.c \ + ibb.h \ iq.c \ iq.h \ jabber.c \
--- a/libpurple/protocols/jabber/Makefile.mingw Wed Oct 22 15:23:17 2008 +0000 +++ b/libpurple/protocols/jabber/Makefile.mingw Wed Oct 22 17:47:22 2008 +0000 @@ -51,6 +51,7 @@ data.c \ disco.c \ google.c \ + ibb.c \ iq.c \ jabber.c \ jutil.c \
--- a/libpurple/protocols/jabber/disco.c Wed Oct 22 15:23:17 2008 +0000 +++ b/libpurple/protocols/jabber/disco.c Wed Oct 22 17:47:22 2008 +0000 @@ -132,9 +132,7 @@ SUPPORT_FEATURE("http://jabber.org/protocol/bytestreams") SUPPORT_FEATURE("http://jabber.org/protocol/disco#info") SUPPORT_FEATURE("http://jabber.org/protocol/disco#items") -#if 0 - SUPPORT_FEATURE("http://jabber.org/protocol/ibb") -#endif + SUPPORT_FEATURE("http://jabber.org/protocol/ibb"); SUPPORT_FEATURE("http://jabber.org/protocol/muc") SUPPORT_FEATURE("http://jabber.org/protocol/muc#user") SUPPORT_FEATURE("http://jabber.org/protocol/si") @@ -273,6 +271,10 @@ else if(!strcmp(var, "http://jabber.org/protocol/commands")) { capabilities |= JABBER_CAP_ADHOC; } + else if(!strcmp(var, "http://jabber.org/protocol/ibb")) { + purple_debug_info("jabber", "remote supports IBB\n"); + capabilities |= JABBER_CAP_IBB; + } } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/ibb.c Wed Oct 22 17:47:22 2008 +0000 @@ -0,0 +1,534 @@ +/* + * 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 Library 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 <glib.h> +#include <string.h> + +#include "internal.h" +#include "ibb.h" +#include "debug.h" +#include "xmlnode.h" + +#define JABBER_IBB_SESSION_DEFAULT_BLOCK_SIZE 4096 + +static GHashTable *jabber_ibb_sessions = NULL; +static GList *open_handlers = NULL; + +JabberIBBSession * +jabber_ibb_session_create(JabberStream *js, const gchar *sid, const gchar *who, + gpointer user_data) +{ + JabberIBBSession *sess = g_new0(JabberIBBSession, 1); + + if (!sess) { + purple_debug_error("jabber", "Could not allocate IBB session object\n"); + return NULL; + } + + sess->js = js; + if (sid) { + sess->sid = g_strdup(sid); + } else { + sess->sid = g_strdup(jabber_get_next_id(js)); + } + sess->who = g_strdup(who); + sess->block_size = JABBER_IBB_SESSION_DEFAULT_BLOCK_SIZE; + sess->state = JABBER_IBB_SESSION_NOT_OPENED; + sess->user_data = user_data; + + g_hash_table_insert(jabber_ibb_sessions, sess->sid, sess); + + return sess; +} + +JabberIBBSession * +jabber_ibb_session_create_from_xmlnode(JabberStream *js, xmlnode *packet, + gpointer user_data) +{ + JabberIBBSession *sess = NULL; + xmlnode *open = xmlnode_get_child_with_namespace(packet, "open", + XEP_0047_NAMESPACE); + const gchar *sid = xmlnode_get_attrib(open, "sid"); + const gchar *block_size = xmlnode_get_attrib(open, "block-size"); + gsize block_size_int; + + if (!open) { + return NULL; + } + + sess = g_new0(JabberIBBSession, 1); + + if (!sess) { + purple_debug_error("jabber", "Could not allocate IBB session object\n"); + return NULL; + } + + if (!sid || !block_size) { + purple_debug_error("jabber", + "IBB session open tag requires sid and block-size attributes\n"); + g_free(sess); + return NULL; + } + + block_size_int = atoi(block_size); + sess->js = js; + sess->sid = g_strdup(sid); + sess->id = g_strdup(xmlnode_get_attrib(packet, "id")); + sess->who = g_strdup(xmlnode_get_attrib(packet, "from")); + sess->block_size = block_size_int; + /* if we create a session from an incoming <open/> request, it means the + session is immediatly open... */ + sess->state = JABBER_IBB_SESSION_OPENED; + sess->user_data = user_data; + + g_hash_table_insert(jabber_ibb_sessions, sess->sid, sess); + + return sess; +} + +void +jabber_ibb_session_destroy(JabberIBBSession *sess) +{ + purple_debug_info("jabber", "IBB: destroying session %p %s\n", sess, + sess->sid); + + if (jabber_ibb_session_get_state(sess) == JABBER_IBB_SESSION_OPENED) { + jabber_ibb_session_close(sess); + } + + purple_debug_info("jabber", "IBB: last_iq_id: %s\n", sess->last_iq_id); + if (sess->last_iq_id) { + purple_debug_info("jabber", "IBB: removing callback for <iq/> %s\n", + sess->last_iq_id); + jabber_iq_remove_callback_by_id(jabber_ibb_session_get_js(sess), + sess->last_iq_id); + g_free(sess->last_iq_id); + sess->last_iq_id = NULL; + } + + g_hash_table_remove(jabber_ibb_sessions, sess->sid); + g_free(sess->sid); + g_free(sess->who); + g_free(sess); +} + +const gchar * +jabber_ibb_session_get_sid(const JabberIBBSession *sess) +{ + return sess->sid; +} + +JabberStream * +jabber_ibb_session_get_js(JabberIBBSession *sess) +{ + return sess->js; +} + +const gchar * +jabber_ibb_session_get_who(const JabberIBBSession *sess) +{ + return sess->who; +} + +guint16 +jabber_ibb_session_get_send_seq(const JabberIBBSession *sess) +{ + return sess->send_seq; +} + +guint16 +jabber_ibb_session_get_recv_seq(const JabberIBBSession *sess) +{ + return sess->recv_seq; +} + +JabberIBBSessionState +jabber_ibb_session_get_state(const JabberIBBSession *sess) +{ + return sess->state; +} + +gsize +jabber_ibb_session_get_block_size(const JabberIBBSession *sess) +{ + return sess->block_size; +} + +void +jabber_ibb_session_set_block_size(JabberIBBSession *sess, gsize size) +{ + if (jabber_ibb_session_get_state(sess) == JABBER_IBB_SESSION_NOT_OPENED) { + sess->block_size = size; + } else { + purple_debug_error("jabber", + "Can't set block size on an open IBB session\n"); + } +} + +gpointer +jabber_ibb_session_get_user_data(JabberIBBSession *sess) +{ + return sess->user_data; +} + +void +jabber_ibb_session_set_opened_callback(JabberIBBSession *sess, + JabberIBBOpenedCallback *cb) +{ + sess->opened_cb = cb; +} + +void +jabber_ibb_session_set_data_sent_callback(JabberIBBSession *sess, + JabberIBBSentCallback *cb) +{ + sess->data_sent_cb = cb; +} + +void +jabber_ibb_session_set_closed_callback(JabberIBBSession *sess, + JabberIBBClosedCallback *cb) +{ + sess->closed_cb = cb; +} + +void +jabber_ibb_session_set_data_received_callback(JabberIBBSession *sess, + JabberIBBDataCallback *cb) +{ + sess->data_received_cb = cb; +} + +void +jabber_ibb_session_set_error_callback(JabberIBBSession *sess, + JabberIBBErrorCallback *cb) +{ + sess->error_cb = cb; +} + +static void +jabber_ibb_session_opened_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + JabberIBBSession *sess = (JabberIBBSession *) data; + + if (strcmp(xmlnode_get_attrib(packet, "type"), "error") == 0) { + sess->state = JABBER_IBB_SESSION_ERROR; + } else { + sess->state = JABBER_IBB_SESSION_OPENED; + } + + if (sess->opened_cb) { + sess->opened_cb(sess); + } +} + +void +jabber_ibb_session_open(JabberIBBSession *sess) +{ + if (jabber_ibb_session_get_state(sess) != JABBER_IBB_SESSION_NOT_OPENED) { + purple_debug_error("jabber", + "jabber_ibb_session called on an already open stream\n"); + } else { + JabberIq *set = jabber_iq_new(sess->js, JABBER_IQ_SET); + xmlnode *open = xmlnode_new("open"); + gchar block_size[10]; + + xmlnode_set_attrib(set->node, "to", jabber_ibb_session_get_who(sess)); + xmlnode_set_namespace(open, XEP_0047_NAMESPACE); + xmlnode_set_attrib(open, "sid", jabber_ibb_session_get_sid(sess)); + g_snprintf(block_size, sizeof(block_size), "%ld", + jabber_ibb_session_get_block_size(sess)); + xmlnode_set_attrib(open, "block-size", block_size); + xmlnode_insert_child(set->node, open); + + jabber_iq_set_callback(set, jabber_ibb_session_opened_cb, sess); + + jabber_iq_send(set); + } +} + +void +jabber_ibb_session_close(JabberIBBSession *sess) +{ + JabberIBBSessionState state = jabber_ibb_session_get_state(sess); + + if (state != JABBER_IBB_SESSION_OPENED && state != JABBER_IBB_SESSION_ERROR) { + purple_debug_error("jabber", + "jabber_ibb_session_close called on a session that has not been" + "opened\n"); + } else { + JabberIq *set = jabber_iq_new(jabber_ibb_session_get_js(sess), + JABBER_IQ_SET); + xmlnode *close = xmlnode_new("close"); + + xmlnode_set_attrib(set->node, "to", jabber_ibb_session_get_who(sess)); + xmlnode_set_namespace(close, XEP_0047_NAMESPACE); + xmlnode_set_attrib(close, "sid", jabber_ibb_session_get_sid(sess)); + xmlnode_insert_child(set->node, close); + jabber_iq_send(set); + sess->state = JABBER_IBB_SESSION_CLOSED; + } +} + +void +jabber_ibb_session_accept(JabberIBBSession *sess) +{ + JabberIq *result = jabber_iq_new(jabber_ibb_session_get_js(sess), + JABBER_IQ_RESULT); + + xmlnode_set_attrib(result->node, "to", jabber_ibb_session_get_who(sess)); + jabber_iq_set_id(result, sess->id); + jabber_iq_send(result); + sess->state = JABBER_IBB_SESSION_OPENED; +} + +static void +jabber_ibb_session_send_acknowledge_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + JabberIBBSession *sess = (JabberIBBSession *) data; + xmlnode *error = xmlnode_get_child(packet, "error"); + + if (sess) { + /* reset callback */ + if (sess->last_iq_id) { + g_free(sess->last_iq_id); + sess->last_iq_id = NULL; + } + + if (error) { + jabber_ibb_session_close(sess); + sess->state = JABBER_IBB_SESSION_ERROR; + + if (sess->error_cb) { + sess->error_cb(sess); + } + } else { + if (sess->data_sent_cb) { + sess->data_sent_cb(sess); + } + } + } else { + /* the session has gone away, it was probably cancelled */ + purple_debug_info("jabber", + "got response from send data, but IBB session is no longer active\n"); + } +} + +void +jabber_ibb_session_send_data(JabberIBBSession *sess, gpointer data, gsize size) +{ + JabberIBBSessionState state = jabber_ibb_session_get_state(sess); + + purple_debug_info("jabber", "sending data block of %ld bytes on IBB stream\n", + size); + + if (state != JABBER_IBB_SESSION_OPENED) { + purple_debug_error("jabber", + "trying to send data on a non-open IBB session\n"); + } else if (size > jabber_ibb_session_get_block_size(sess)) { + purple_debug_error("jabber", + "trying to send a too large packet in the IBB session\n"); + } else { + JabberIq *set = jabber_iq_new(jabber_ibb_session_get_js(sess), + JABBER_IQ_SET); + xmlnode *data_element = xmlnode_new("data"); + char *base64 = purple_base64_encode(data, size); + char seq[10]; + g_snprintf(seq, sizeof(seq), "%d", jabber_ibb_session_get_send_seq(sess)); + + xmlnode_set_attrib(set->node, "to", jabber_ibb_session_get_who(sess)); + xmlnode_set_namespace(data_element, XEP_0047_NAMESPACE); + xmlnode_set_attrib(data_element, "sid", jabber_ibb_session_get_sid(sess)); + xmlnode_set_attrib(data_element, "seq", seq); + xmlnode_insert_data(data_element, base64, strlen(base64)); + + xmlnode_insert_child(set->node, data_element); + + purple_debug_info("jabber", + "IBB: setting send <iq/> callback for session %p %s\n", sess, + sess->sid); + jabber_iq_set_callback(set, jabber_ibb_session_send_acknowledge_cb, sess); + sess->last_iq_id = g_strdup(xmlnode_get_attrib(set->node, "id")); + purple_debug_info("jabber", "IBB: set sess->last_iq_id: %s %s\n", + sess->last_iq_id, xmlnode_get_attrib(set->node, "id")); + jabber_iq_send(set); + + g_free(base64); + (sess->send_seq)++; + } +} + +static void +jabber_ibb_send_error_response(JabberStream *js, xmlnode *packet) +{ + JabberIq *result = jabber_iq_new(js, JABBER_IQ_ERROR); + xmlnode *error = xmlnode_new("error"); + xmlnode *item_not_found = xmlnode_new("item-not-found"); + + xmlnode_set_namespace(item_not_found, + "urn:ietf:params:xml:ns:xmpp-stanzas"); + xmlnode_set_attrib(error, "code", "440"); + xmlnode_set_attrib(error, "type", "cancel"); + jabber_iq_set_id(result, xmlnode_get_attrib(packet, "id")); + xmlnode_set_attrib(result->node, "to", + xmlnode_get_attrib(packet, "from")); + xmlnode_insert_child(error, item_not_found); + xmlnode_insert_child(result->node, error); + + jabber_iq_send(result); +} + +void +jabber_ibb_parse(JabberStream *js, xmlnode *packet) +{ + xmlnode *data = xmlnode_get_child_with_namespace(packet, "data", + XEP_0047_NAMESPACE); + xmlnode *close = xmlnode_get_child_with_namespace(packet, "close", + XEP_0047_NAMESPACE); + xmlnode *open = xmlnode_get_child_with_namespace(packet, "open", + XEP_0047_NAMESPACE); + const gchar *sid = + data ? xmlnode_get_attrib(data, "sid") : + close ? xmlnode_get_attrib(close, "sid") : NULL; + JabberIBBSession *sess = + sid ? g_hash_table_lookup(jabber_ibb_sessions, sid) : NULL; + const gchar *who = xmlnode_get_attrib(packet, "from"); + + if (sess) { + + if (strcmp(who, jabber_ibb_session_get_who(sess)) != 0) { + /* the iq comes from a different JID than the remote JID of the + session, ignore it */ + purple_debug_error("jabber", + "Got IBB iq from wrong JID, ignoring\n"); + } else if (data) { + guint16 seq = atoi(xmlnode_get_attrib(data, "seq")); + + /* reject the data, and set the session in error if we get an + out-of-order packet */ + if (seq == jabber_ibb_session_get_recv_seq(sess)) { + /* sequence # is the expected... */ + JabberIq *result = jabber_iq_new(js, JABBER_IQ_RESULT); + + jabber_iq_set_id(result, xmlnode_get_attrib(packet, "id")); + xmlnode_set_attrib(result->node, "to", + xmlnode_get_attrib(packet, "from")); + + if (sess->data_received_cb) { + gchar *base64 = xmlnode_get_data(data); + gsize size; + gpointer rawdata = purple_base64_decode(base64, &size); + + g_free(base64); + + if (rawdata) { + purple_debug_info("jabber", + "got %ld bytes of data on IBB stream\n", size); + if (size > jabber_ibb_session_get_block_size(sess)) { + purple_debug_error("jabber", + "IBB: received a too large packet\n"); + } else { + purple_debug_info("jabber", + "calling IBB callback for received data\n"); + sess->data_received_cb(sess, rawdata, size); + } + g_free(rawdata); + } else { + purple_debug_error("jabber", + "IBB: invalid BASE64 data received\n"); + } + } + + (sess->recv_seq)++; + jabber_iq_send(result); + + } else { + purple_debug_error("jabber", + "Received an out-of-order IBB packet\n"); + sess->state = JABBER_IBB_SESSION_ERROR; + + if (sess->error_cb) { + sess->error_cb(sess); + } + } + } else if (close) { + sess->state = JABBER_IBB_SESSION_CLOSED; + purple_debug_info("jabber", "IBB: received close\n"); + + if (sess->closed_cb) { + purple_debug_info("jabber", "IBB: calling closed handler\n"); + sess->closed_cb(sess); + } + + } else { + /* this should never happen */ + purple_debug_error("jabber", "Received bogus iq for IBB session\n"); + } + } else if (open) { + JabberIq *result; + const GList *iterator; + + /* run all open handlers registered until one returns true */ + for (iterator = open_handlers ; iterator ; + iterator = g_list_next(iterator)) { + JabberIBBOpenHandler *handler = (JabberIBBOpenHandler *) iterator->data; + + if (handler(js, packet)) { + result = jabber_iq_new(js, JABBER_IQ_RESULT); + xmlnode_set_attrib(result->node, "to", + xmlnode_get_attrib(packet, "from")); + jabber_iq_set_id(result, xmlnode_get_attrib(packet, "id")); + jabber_iq_send(result); + return; + } + } + /* no open callback returned success, reject */ + jabber_ibb_send_error_response(js, packet); + } else { + /* send error reply */ + jabber_ibb_send_error_response(js, packet); + } +} + +void +jabber_ibb_register_open_handler(JabberIBBOpenHandler *cb) +{ + open_handlers = g_list_append(open_handlers, cb); +} + +void +jabber_ibb_unregister_open_handler(JabberIBBOpenHandler *cb) +{ + open_handlers = g_list_remove(open_handlers, cb); +} + +void +jabber_ibb_init(void) +{ + jabber_ibb_sessions = g_hash_table_new(g_str_hash, g_str_equal); +} + +void +jabber_ibb_uninit(void) +{ + g_hash_table_destroy(jabber_ibb_sessions); + g_list_free(open_handlers); + jabber_ibb_sessions = NULL; + open_handlers = NULL; +} + + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/ibb.h Wed Oct 22 17:47:22 2008 +0000 @@ -0,0 +1,119 @@ +/* + * 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 Library 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 "iq.h" + +#ifndef JABBER_IBB_SESSION +#define JABBER_IBB_SESSION + +#define XEP_0047_NAMESPACE "http://jabber.org/protocol/ibb" + +typedef struct _JabberIBBSession JabberIBBSession; + +typedef void +(JabberIBBDataCallback)(JabberIBBSession *, const gpointer data, gsize size); + +typedef void (JabberIBBOpenedCallback)(JabberIBBSession *); +typedef void (JabberIBBClosedCallback)(JabberIBBSession *); +typedef void (JabberIBBErrorCallback)(JabberIBBSession *); +typedef void (JabberIBBSentCallback)(JabberIBBSession *); + +typedef gboolean (JabberIBBOpenHandler)(JabberStream *js, xmlnode *packet); + +typedef enum { + JABBER_IBB_SESSION_NOT_OPENED, + JABBER_IBB_SESSION_OPENED, + JABBER_IBB_SESSION_CLOSED, + JABBER_IBB_SESSION_ERROR +} JabberIBBSessionState; + +struct _JabberIBBSession { + JabberStream *js; + gchar *who; + gchar *sid; + gchar *id; + guint16 send_seq; + guint16 recv_seq; + gsize block_size; + + /* session state */ + JabberIBBSessionState state; + + /* user data (f.ex. a handle to a PurpleXfer) */ + gpointer user_data; + + /* callbacks */ + JabberIBBOpenedCallback *opened_cb; + JabberIBBSentCallback *data_sent_cb; + JabberIBBClosedCallback *closed_cb; + /* callback for receiving data */ + JabberIBBDataCallback *data_received_cb; + JabberIBBErrorCallback *error_cb; + + /* store the last sent IQ (to permit cancel of callback) */ + gchar *last_iq_id; +}; + +JabberIBBSession *jabber_ibb_session_create(JabberStream *js, const gchar *sid, + const gchar *who, gpointer user_data); +JabberIBBSession *jabber_ibb_session_create_from_xmlnode(JabberStream *js, + xmlnode *packet, gpointer user_data); + +void jabber_ibb_session_destroy(JabberIBBSession *sess); + +void jabber_ibb_session_set_opened_callback(JabberIBBSession *sess, + JabberIBBOpenedCallback *cb); +void jabber_ibb_session_set_data_sent_callback(JabberIBBSession *sess, + JabberIBBSentCallback *cb); +void jabber_ibb_session_set_closed_callback(JabberIBBSession *sess, + JabberIBBClosedCallback *cb); +void jabber_ibb_session_set_data_received_callback(JabberIBBSession *sess, + JabberIBBDataCallback *cb); +void jabber_ibb_session_set_error_callback(JabberIBBSession *sess, + JabberIBBErrorCallback *cb); + +void jabber_ibb_session_open(JabberIBBSession *sess); +void jabber_ibb_session_close(JabberIBBSession *sess); +void jabber_ibb_session_accept(JabberIBBSession *sess); +void jabber_ibb_session_send_data(JabberIBBSession *sess, gpointer data, + gsize size); + +const gchar *jabber_ibb_session_get_sid(const JabberIBBSession *sess); +JabberStream *jabber_ibb_session_get_js(JabberIBBSession *sess); +const gchar *jabber_ibb_session_get_who(const JabberIBBSession *sess); + +guint16 jabber_ibb_session_get_send_seq(const JabberIBBSession *sess); +guint16 jabber_ibb_session_get_recv_seq(const JabberIBBSession *sess); + +JabberIBBSessionState jabber_ibb_session_get_state(const JabberIBBSession *sess); + +gsize jabber_ibb_session_get_block_size(const JabberIBBSession *sess); +void jabber_ibb_session_set_block_size(JabberIBBSession *sess, gsize size); + +gpointer jabber_ibb_session_get_user_data(JabberIBBSession *sess); + +/* handle incoming packet */ +void jabber_ibb_parse(JabberStream *js, xmlnode *packet); + +/* add a handler for open session */ +void jabber_ibb_register_open_handler(JabberIBBOpenHandler *cb); +void jabber_ibb_unregister_open_handler(JabberIBBOpenHandler *cb); + +void jabber_ibb_init(void); +void jabber_ibb_uninit(void); + +#endif /* JABBER_IBB_SESSION */
--- a/libpurple/protocols/jabber/iq.c Wed Oct 22 15:23:17 2008 +0000 +++ b/libpurple/protocols/jabber/iq.c Wed Oct 22 17:47:22 2008 +0000 @@ -34,6 +34,7 @@ #include "ping.h" #include "adhoccommands.h" #include "data.h" +#include "ibb.h" #ifdef _WIN32 #include "utsname.h" @@ -361,6 +362,13 @@ return; } + if (xmlnode_get_child_with_namespace(packet, "data", XEP_0047_NAMESPACE) + || xmlnode_get_child_with_namespace(packet, "close", XEP_0047_NAMESPACE) + || xmlnode_get_child_with_namespace(packet, "open", XEP_0047_NAMESPACE)) { + jabber_ibb_parse(js, packet); + return; + } + /* If we get here, send the default error reply mandated by XMPP-CORE */ if(type && (!strcmp(type, "set") || !strcmp(type, "get"))) { JabberIq *iq = jabber_iq_new(js, JABBER_IQ_ERROR);
--- a/libpurple/protocols/jabber/libxmpp.c Wed Oct 22 15:23:17 2008 +0000 +++ b/libpurple/protocols/jabber/libxmpp.c Wed Oct 22 17:47:22 2008 +0000 @@ -44,6 +44,7 @@ #include "usertune.h" #include "caps.h" #include "data.h" +#include "ibb.h" static PurplePluginProtocolInfo prpl_info = { @@ -150,6 +151,8 @@ purple_signal_unregister(plugin, "jabber-sending-text"); jabber_data_uninit(); + jabber_si_uninit(); + jabber_ibb_uninit(); return TRUE; } @@ -281,12 +284,17 @@ jabber_data_init(); + + jabber_ibb_init(); + jabber_si_init(); + jabber_add_feature("avatarmeta", AVATARNAMESPACEMETA, jabber_pep_namespace_only_when_pep_enabled_cb); jabber_add_feature("avatardata", AVATARNAMESPACEDATA, jabber_pep_namespace_only_when_pep_enabled_cb); jabber_add_feature("buzz", "http://www.xmpp.org/extensions/xep-0224.html#ns", jabber_buzz_isenabled); jabber_add_feature("bob", XEP_0231_NAMESPACE, jabber_custom_smileys_isenabled); + jabber_add_feature("ibb", XEP_0047_NAMESPACE, NULL); jabber_pep_register_handler("avatar", AVATARNAMESPACEMETA, jabber_buddy_avatar_update_metadata); }
--- a/libpurple/protocols/jabber/si.c Wed Oct 22 15:23:17 2008 +0000 +++ b/libpurple/protocols/jabber/si.c Wed Oct 22 17:47:22 2008 +0000 @@ -35,6 +35,7 @@ #include "jabber.h" #include "iq.h" #include "si.h" +#include "ibb.h" #define STREAMHOST_CONNECT_TIMEOUT 15 @@ -64,8 +65,14 @@ size_t rxlen; gsize rxmaxlen; int local_streamhost_fd; + + JabberIBBSession *ibb_session; + FILE *fp; } JabberSIXfer; +/* some forward declarations */ +static void jabber_si_xfer_ibb_send_init(JabberStream *js, PurpleXfer *xfer); + static PurpleXfer* jabber_si_xfer_find(JabberStream *js, const char *sid, const char *from) { @@ -204,8 +211,25 @@ jabber_iq_send(iq); - purple_xfer_cancel_local(xfer); - + /* if IBB is available, revert to that before giving up... */ + if (jsx->stream_method & STREAM_METHOD_IBB) { + /* if we are the initializer, init IBB */ + purple_debug_info("jabber", + "jabber_si_bytestreams_attempt_connect: " + "no streamhosts found, trying IBB\n"); + /* if we are the sender, open an IBB session. But not if we already + done it, since we could have received the error <iq/> from the + receiver already... */ + if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND + && !jsx->ibb_session) { + jabber_si_xfer_ibb_send_init(jsx->js, xfer); + } + /* if we are the receiver, just wait for IBB open, callback is + already set up... */ + } else { + purple_xfer_cancel_local(xfer); + } + return; } @@ -666,8 +690,29 @@ jsx = xfer->data; if(!(type = xmlnode_get_attrib(packet, "type")) || strcmp(type, "result")) { - if (type && !strcmp(type, "error")) - purple_xfer_cancel_remote(xfer); + purple_debug_info("jabber", + "jabber_si_xfer_connect_proxy_cb: type = %s\n", + type); + if (type && !strcmp(type, "error")) { + /* if IBB is available, open IBB session */ + purple_debug_info("jabber", + "jabber_si_xfer_connect_proxy_cb: got error, method: %d\n", + jsx->stream_method); + if (jsx->stream_method & STREAM_METHOD_IBB) { + purple_debug_info("jabber", "IBB is possible, try it\n"); + /* if we are the sender and haven't already opened an IBB + session, do so now (we might already have failed to open + the bytestream proxy ourselves when receiving this <iq/> */ + if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND + && !jsx->ibb_session) { + jabber_si_xfer_ibb_send_init(js, xfer); + } + /* if we are receiver, just wait for IBB open stanza, callback + is already set up */ + } else { + purple_xfer_cancel_remote(xfer); + } + } return; } @@ -694,8 +739,19 @@ purple_debug_info("jabber", "Got local SOCKS5 streamhost-used.\n"); purple_xfer_start(xfer, xfer->fd, NULL, -1); } else { - purple_debug_info("jabber", "streamhost-used does not match any proxy that was offered to target\n"); - purple_xfer_cancel_local(xfer); + /* if available, try to revert to IBB... */ + if (jsx->stream_method & STREAM_METHOD_IBB) { + purple_debug_info("jabber", + "jabber_si_connect_proxy_cb: trying to revert to IBB\n"); + if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND) { + jabber_si_xfer_ibb_send_init(jsx->js, xfer); + } + /* if we are the receiver, we are already set up...*/ + } else { + purple_debug_info("jabber", + "streamhost-used does not match any proxy that was offered to target\n"); + purple_xfer_cancel_local(xfer); + } } g_free(my_jid); return; @@ -822,8 +878,23 @@ /* We have no way of transferring, cancel the transfer */ if (streamhost_count == 0) { jabber_iq_free(iq); - /* We should probably notify the target, but this really shouldn't ever happen */ - purple_xfer_cancel_local(xfer); + + /* if available, revert to IBB */ + if (jsx->stream_method & STREAM_METHOD_IBB) { + purple_debug_info("jabber", + "jabber_si_xfer_bytestreams_listen_cb: trying to revert to IBB\n"); + if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND) { + /* if we are the sender, init the IBB session... */ + jabber_si_xfer_ibb_send_init(jsx->js, xfer); + } + /* if we are the receiver, we should just wait... the IBB open + handler has already been set up... */ + } else { + /* We should probably notify the target, + but this really shouldn't ever happen */ + purple_xfer_cancel_local(xfer); + } + return; } @@ -853,11 +924,232 @@ } +/* forward declare some functions here... */ +static void jabber_si_xfer_cancel_recv(PurpleXfer *xfer); +static void jabber_si_xfer_cancel_send(PurpleXfer *xfer); +static void jabber_si_xfer_free(PurpleXfer *xfer); + +static void +jabber_si_xfer_ibb_error_cb(JabberIBBSession *sess) +{ + PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess); + JabberStream *js = jabber_ibb_session_get_js(sess); + PurpleConnection *gc = js->gc; + PurpleAccount *account = purple_connection_get_account(gc); + + purple_debug_error("jabber", "an error occured during IBB file transfer\n"); + purple_xfer_error(purple_xfer_get_type(xfer), account, + jabber_ibb_session_get_who(sess), + _("An error occured on the in-band bytestream transfer\n")); + purple_xfer_end(xfer); +} + +static void +jabber_si_xfer_ibb_closed_cb(JabberIBBSession *sess) +{ + PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess); + JabberStream *js = jabber_ibb_session_get_js(sess); + PurpleConnection *gc = js->gc; + PurpleAccount *account = purple_connection_get_account(gc); + + purple_debug_info("jabber", "the remote user closed the transfer\n"); + if (purple_xfer_get_bytes_remaining(xfer) > 0) { + purple_xfer_error(purple_xfer_get_type(xfer), account, + jabber_ibb_session_get_who(sess), _("Transfer was closed.")); + } else { + purple_xfer_set_completed(xfer, TRUE); + } + jabber_si_xfer_free(xfer); + purple_xfer_end(xfer); +} + +static void +jabber_si_xfer_ibb_recv_data_cb(JabberIBBSession *sess, gpointer data, + gsize size) +{ + PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess); + JabberSIXfer *jsx = (JabberSIXfer *) xfer->data; + + if (size <= purple_xfer_get_bytes_remaining(xfer)) { + purple_debug_info("jabber", "about to write %lu bytes from IBB stream\n", + size); + if(!fwrite(data, size, 1, jsx->fp)) { + purple_debug_error("jabber", "error writing to file\n"); + jabber_si_xfer_cancel_recv(xfer); + return; + } + purple_xfer_set_bytes_sent(xfer, purple_xfer_get_bytes_sent(xfer) + size); + purple_xfer_update_progress(xfer); + + if (purple_xfer_get_bytes_remaining(xfer) == 0) { + purple_xfer_set_completed(xfer, TRUE); + jabber_si_xfer_free(xfer); + purple_xfer_end(xfer); + } + } else { + /* trying to write past size of file transfers negotiated size, + reject transfer to protect against malicious behaviour */ + purple_debug_error("jabber", + "IBB file transfer, trying to write past end of file\n"); + jabber_si_xfer_cancel_recv(xfer); + purple_xfer_end(xfer); + } + +} + +static gboolean +jabber_si_xfer_ibb_open_cb(JabberStream *js, xmlnode *packet) +{ + const gchar *who = xmlnode_get_attrib(packet, "from"); + xmlnode *open = xmlnode_get_child(packet, "open"); + const gchar *sid = xmlnode_get_attrib(open, "sid"); + PurpleXfer *xfer = jabber_si_xfer_find(js, sid, who); + if (xfer) { + JabberSIXfer *jsx = (JabberSIXfer *) xfer->data; + JabberIBBSession *sess = + jabber_ibb_session_create_from_xmlnode(js, packet, xfer); + + if (sess) { + /* setup callbacks here...*/ + jabber_ibb_session_set_data_received_callback(sess, + jabber_si_xfer_ibb_recv_data_cb); + jabber_ibb_session_set_closed_callback(sess, + jabber_si_xfer_ibb_closed_cb); + jabber_ibb_session_set_error_callback(sess, + jabber_si_xfer_ibb_error_cb); + + /* open the file to write to */ + jsx->fp = g_fopen(purple_xfer_get_local_filename(xfer), "wb"); + + jsx->ibb_session = sess; + + /* start the transfer */ + purple_xfer_start(xfer, 0, NULL, 0); + return TRUE; + } else { + /* failed to create IBB session */ + purple_debug_error("jabber", "failed to create IBB session\n"); + jabber_si_xfer_cancel_recv(xfer); + purple_xfer_end(xfer); + return FALSE; + } + } else { + /* we got an IBB <open/> for an unknown file transfer, pass along... */ + purple_debug_info("jabber", + "IBB open did not match any SI file transfer\n"); + return FALSE; + } +} + +static void +jabber_si_xfer_ibb_send_data(JabberIBBSession *sess) +{ + PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess); + JabberSIXfer *jsx = (JabberSIXfer *) xfer->data; + gsize remaining = purple_xfer_get_bytes_remaining(xfer); + gsize packet_size = remaining < jabber_ibb_session_get_block_size(sess) ? + remaining : jabber_ibb_session_get_block_size(sess); + gpointer data = g_malloc(packet_size); + int res; + + purple_debug_info("jabber", "IBB: about to read %lu bytes from file %p\n", + packet_size, jsx->fp); + res = fread(data, packet_size, 1, jsx->fp); + + if (res == 1) { + jabber_ibb_session_send_data(sess, data, packet_size); + purple_xfer_set_bytes_sent(xfer, + purple_xfer_get_bytes_sent(xfer) + packet_size); + purple_xfer_update_progress(xfer); + } else { + purple_debug_error("jabber", + "jabber_si_xfer_ibb_send_data: error reading from file\n"); + jabber_si_xfer_cancel_send(xfer); + } +} + +static void +jabber_si_xfer_ibb_sent_cb(JabberIBBSession *sess) +{ + PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess); + gsize remaining = purple_xfer_get_bytes_remaining(xfer); + + if (remaining == 0) { + /* close the session */ + jabber_ibb_session_close(sess); + purple_xfer_set_completed(xfer, TRUE); + jabber_si_xfer_free(xfer); + purple_xfer_end(xfer); + } else { + /* send more... */ + jabber_si_xfer_ibb_send_data(sess); + } +} + +static void +jabber_si_xfer_ibb_opened_cb(JabberIBBSession *sess) +{ + PurpleXfer *xfer = (PurpleXfer *) jabber_ibb_session_get_user_data(sess); + JabberSIXfer *jsx = (JabberSIXfer *) xfer->data; + + if (jabber_ibb_session_get_state(sess) == JABBER_IBB_SESSION_OPENED) { + purple_xfer_start(xfer, 0, NULL, 0); + purple_xfer_set_bytes_sent(xfer, 0); + purple_xfer_update_progress(xfer); + jsx->fp = g_fopen(purple_xfer_get_local_filename(xfer), "rb"); + jabber_si_xfer_ibb_send_data(sess); + } else { + /* error */ + JabberStream *js = jabber_ibb_session_get_js(sess); + PurpleConnection *gc = js->gc; + PurpleAccount *account = purple_connection_get_account(gc); + jabber_si_xfer_free(xfer); + purple_xfer_error(purple_xfer_get_type(xfer), account, + jabber_ibb_session_get_who(sess), + _("Failed to open in-band bytestream")); + purple_xfer_end(xfer); + } +} + +static void +jabber_si_xfer_ibb_send_init(JabberStream *js, PurpleXfer *xfer) +{ + JabberSIXfer *jsx = (JabberSIXfer *) xfer->data; + + purple_xfer_ref(xfer); + + jsx->ibb_session = jabber_ibb_session_create(js, jsx->stream_id, + purple_xfer_get_remote_user(xfer), xfer); + + if (jsx->ibb_session) { + /* should set callbacks here... */ + jabber_ibb_session_set_opened_callback(jsx->ibb_session, + jabber_si_xfer_ibb_opened_cb); + jabber_ibb_session_set_data_sent_callback(jsx->ibb_session, + jabber_si_xfer_ibb_sent_cb); + jabber_ibb_session_set_closed_callback(jsx->ibb_session, + jabber_si_xfer_ibb_closed_cb); + jabber_ibb_session_set_error_callback(jsx->ibb_session, + jabber_si_xfer_ibb_error_cb); + + /* open the IBB session */ + jabber_ibb_session_open(jsx->ibb_session); + + } else { + /* failed to create IBB session */ + purple_xfer_unref(xfer); + purple_debug_error("jabber", + "failed to initiate IBB session for file transfer\n"); + jabber_si_xfer_cancel_send(xfer); + } +} + static void jabber_si_xfer_send_method_cb(JabberStream *js, xmlnode *packet, gpointer data) { PurpleXfer *xfer = data; xmlnode *si, *feature, *x, *field, *value; + gboolean found_method = FALSE; if(!(si = xmlnode_get_child_with_namespace(packet, "si", "http://jabber.org/protocol/si"))) { purple_xfer_cancel_remote(xfer); @@ -876,20 +1168,33 @@ for(field = xmlnode_get_child(x, "field"); field; field = xmlnode_get_next_twin(field)) { const char *var = xmlnode_get_attrib(field, "var"); - + JabberSIXfer *jsx = (JabberSIXfer *) xfer->data; + if(var && !strcmp(var, "stream-method")) { if((value = xmlnode_get_child(field, "value"))) { char *val = xmlnode_get_data(value); if(val && !strcmp(val, "http://jabber.org/protocol/bytestreams")) { jabber_si_xfer_bytestreams_send_init(xfer); - g_free(val); - return; + jsx->stream_method |= STREAM_METHOD_BYTESTREAMS; + found_method = TRUE; + } else if (val && !strcmp(val, XEP_0047_NAMESPACE)) { + jsx->stream_method |= STREAM_METHOD_IBB; + if (!found_method) { + /* we haven't tried to init a bytestream session, yet + start IBB right away... */ + jabber_si_xfer_ibb_send_init(js, xfer); + found_method = TRUE; + } } g_free(val); } } } - purple_xfer_cancel_remote(xfer); + + if (!found_method) { + purple_xfer_cancel_remote(xfer); + } + } static void jabber_si_xfer_send_request(PurpleXfer *xfer) @@ -929,11 +1234,9 @@ option = xmlnode_new_child(field, "option"); value = xmlnode_new_child(option, "value"); xmlnode_insert_data(value, "http://jabber.org/protocol/bytestreams", -1); - /* option = xmlnode_new_child(field, "option"); value = xmlnode_new_child(option, "value"); xmlnode_insert_data(value, "http://jabber.org/protocol/ibb", -1); - */ jabber_iq_set_callback(iq, jabber_si_xfer_send_method_cb, xfer); @@ -947,38 +1250,60 @@ static void jabber_si_xfer_free(PurpleXfer *xfer) { JabberSIXfer *jsx = xfer->data; - JabberStream *js = jsx->js; - - js->file_transfers = g_list_remove(js->file_transfers, xfer); - - if (jsx->connect_data != NULL) - purple_proxy_connect_cancel(jsx->connect_data); - if (jsx->listen_data != NULL) - purple_network_listen_cancel(jsx->listen_data); - if (jsx->iq_id != NULL) - jabber_iq_remove_callback_by_id(js, jsx->iq_id); - if (jsx->local_streamhost_fd >= 0) - close(jsx->local_streamhost_fd); - if (jsx->connect_timeout > 0) - purple_timeout_remove(jsx->connect_timeout); + + if (jsx) { + JabberStream *js = jsx->js; + + js->file_transfers = g_list_remove(js->file_transfers, xfer); + + if (jsx->connect_data != NULL) + purple_proxy_connect_cancel(jsx->connect_data); + if (jsx->listen_data != NULL) + purple_network_listen_cancel(jsx->listen_data); + if (jsx->iq_id != NULL) + jabber_iq_remove_callback_by_id(js, jsx->iq_id); + if (jsx->local_streamhost_fd >= 0) + close(jsx->local_streamhost_fd); + if (jsx->connect_timeout > 0) + purple_timeout_remove(jsx->connect_timeout); + + if (jsx->streamhosts) { + g_list_foreach(jsx->streamhosts, jabber_si_free_streamhost, NULL); + g_list_free(jsx->streamhosts); + } + + if (jsx->ibb_session) { + purple_debug_info("jabber", + "jabber_si_xfer_free: destroying IBB session\n"); + jabber_ibb_session_destroy(jsx->ibb_session); + } + + if (jsx->fp) { + purple_debug_info("jabber", + "jabber_si_xfer_free: closing file for IBB transfer\n"); + fclose(jsx->fp); + } + + g_free(jsx->stream_id); + g_free(jsx->iq_id); + /* XXX: free other stuff */ + g_free(jsx->rxqueue); + g_free(jsx); + xfer->data = NULL; - if (jsx->streamhosts) { - g_list_foreach(jsx->streamhosts, jabber_si_free_streamhost, NULL); - g_list_free(jsx->streamhosts); + purple_debug_info("jabber", "jabber_si_xfer_free(): freeing jsx %p\n", jsx); } - - g_free(jsx->stream_id); - g_free(jsx->iq_id); - /* XXX: free other stuff */ - g_free(jsx->rxqueue); - g_free(jsx); - xfer->data = NULL; - - purple_debug_info("jabber", "jabber_si_xfer_free(): freeing jsx %p", jsx); } + static void jabber_si_xfer_cancel_send(PurpleXfer *xfer) { + JabberSIXfer *jsx = (JabberSIXfer *) xfer->data; + + /* if there is an IBB session active, send close on that */ + if (jsx->ibb_session) { + jabber_ibb_session_close(jsx->ibb_session); + } jabber_si_xfer_free(xfer); purple_debug(PURPLE_DEBUG_INFO, "jabber", "in jabber_si_xfer_cancel_send\n"); } @@ -993,6 +1318,11 @@ static void jabber_si_xfer_cancel_recv(PurpleXfer *xfer) { + JabberSIXfer *jsx = (JabberSIXfer *) xfer->data; + /* if there is an IBB session active, send close */ + if (jsx->ibb_session) { + jabber_ibb_session_close(jsx->ibb_session); + } jabber_si_xfer_free(xfer); purple_debug(PURPLE_DEBUG_INFO, "jabber", "in jabber_si_xfer_cancel_recv\n"); } @@ -1007,9 +1337,16 @@ static void jabber_si_xfer_send_disco_cb(JabberStream *js, const char *who, JabberCapabilities capabilities, gpointer data) { - PurpleXfer *xfer = data; + PurpleXfer *xfer = (PurpleXfer *) data; + JabberSIXfer *jsx = (JabberSIXfer *) xfer->data; + + if (capabilities & JABBER_CAP_IBB) { + purple_debug_info("jabber", + "jabber_si_xfer_send_disco_cb: remote JID supports IBB\n"); + jsx->stream_method |= STREAM_METHOD_IBB; + } - if(capabilities & JABBER_CAP_SI_FILE_XFER) { + if (capabilities & JABBER_CAP_SI_FILE_XFER) { jabber_si_xfer_send_request(xfer); } else { char *msg = g_strdup_printf(_("Unable to send file to %s, user does not support file transfers"), who); @@ -1136,18 +1473,20 @@ x = xmlnode_new_child(feature, "x"); xmlnode_set_namespace(x, "jabber:x:data"); xmlnode_set_attrib(x, "type", "submit"); - field = xmlnode_new_child(x, "field"); xmlnode_set_attrib(field, "var", "stream-method"); - - value = xmlnode_new_child(field, "value"); - if(jsx->stream_method & STREAM_METHOD_BYTESTREAMS) + + /* we should maybe "remember" if bytestreams has failed before (in the + same session) with this JID, and only present IBB as an option to + avoid unnessesary timeout */ + if (jsx->stream_method & STREAM_METHOD_BYTESTREAMS) { + value = xmlnode_new_child(field, "value"); xmlnode_insert_data(value, "http://jabber.org/protocol/bytestreams", -1); - /* - else if(jsx->stream_method & STREAM_METHOD_IBB) - xmlnode_insert_data(value, "http://jabber.org/protocol/ibb", -1); - */ - + } else if(jsx->stream_method & STREAM_METHOD_IBB) { + value = xmlnode_new_child(field, "value"); + xmlnode_insert_data(value, "http://jabber.org/protocol/ibb", -1); + } + jabber_iq_send(iq); } } @@ -1167,6 +1506,9 @@ xfer->data = jsx = g_new0(JabberSIXfer, 1); jsx->js = js; jsx->local_streamhost_fd = -1; + + jsx->ibb_session = NULL; + jsx->fp = NULL; purple_xfer_set_init_fnc(xfer, jabber_si_xfer_init); purple_xfer_set_cancel_send_fnc(xfer, jabber_si_xfer_cancel_send); @@ -1238,6 +1580,8 @@ jsx = g_new0(JabberSIXfer, 1); jsx->local_streamhost_fd = -1; + + jsx->ibb_session = NULL; for(field = xmlnode_get_child(x, "field"); field; field = xmlnode_get_next_twin(field)) { const char *var = xmlnode_get_attrib(field, "var"); @@ -1249,10 +1593,8 @@ if((val = xmlnode_get_data(value))) { if(!strcmp(val, "http://jabber.org/protocol/bytestreams")) { jsx->stream_method |= STREAM_METHOD_BYTESTREAMS; - /* } else if(!strcmp(val, "http://jabber.org/protocol/ibb")) { jsx->stream_method |= STREAM_METHOD_IBB; - */ } g_free(val); } @@ -1290,4 +1632,15 @@ } } +void +jabber_si_init(void) +{ + jabber_ibb_register_open_handler(jabber_si_xfer_ibb_open_cb); +} +void +jabber_si_uninit(void) +{ + jabber_ibb_unregister_open_handler(jabber_si_xfer_ibb_open_cb); +} +
--- a/libpurple/protocols/jabber/si.h Wed Oct 22 15:23:17 2008 +0000 +++ b/libpurple/protocols/jabber/si.h Wed Oct 22 17:47:22 2008 +0000 @@ -30,5 +30,7 @@ void jabber_si_parse(JabberStream *js, xmlnode *packet); PurpleXfer *jabber_si_new_xfer(PurpleConnection *gc, const char *who); void jabber_si_xfer_send(PurpleConnection *gc, const char *who, const char *file); +void jabber_si_init(void); +void jabber_si_uninit(void); #endif /* _PURPLE_JABBER_SI_H_ */