Mercurial > pidgin
changeset 25676:394252b681bc
propagate from branch 'im.pidgin.pidgin' (head be64dc9cd9d255a5a6a0f790f5fc505091313e6d)
to branch 'im.pidgin.cpw.darkrain42.xmpp.bosh' (head 6ea1275dfc036db492ab63dcbef17f47a40a94d7)
author | Paul Aurich <paul@darkrain42.org> |
---|---|
date | Tue, 17 Feb 2009 03:39:22 +0000 |
parents | 6a369035fd20 (diff) 93463c6114aa (current diff) |
children | 7d2e85f78aec |
files | libpurple/protocols/jabber/buddy.c libpurple/protocols/jabber/jabber.c |
diffstat | 25 files changed, 2296 insertions(+), 761 deletions(-) [+] |
line wrap: on
line diff
--- a/libpurple/dnssrv.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/dnssrv.c Tue Feb 17 03:39:22 2009 +0000 @@ -33,12 +33,18 @@ #ifndef T_SRV #define T_SRV 33 #endif -#else +#ifndef T_TXT +#define T_TXT 16 +#endif +#else /* WIN32 */ #include <windns.h> /* Missing from the mingw headers */ #ifndef DNS_TYPE_SRV # define DNS_TYPE_SRV 33 #endif +#ifndef DNS_TYPE_TXT +# define DNS_TYPE_TXT 16 +#endif #endif #include "dnssrv.h" @@ -60,9 +66,14 @@ #endif struct _PurpleSrvQueryData { - PurpleSrvCallback cb; + union { + PurpleSrvCallback srv; + PurpleTxtCallback txt; + } cb; + gpointer extradata; guint handle; + int type; #ifdef _WIN32 GThread *resolver; char *query; @@ -74,6 +85,11 @@ #endif }; +typedef struct _PurpleSrvInternalQuery { + int type; + char query[256]; +} PurpleSrvInternalQuery; + static gint responsecompare(gconstpointer ar, gconstpointer br) { @@ -99,6 +115,7 @@ { GList *ret = NULL; PurpleSrvResponse *srvres; + PurpleTxtResponse *txtres; queryans answer; int size; int qdcount; @@ -107,23 +124,22 @@ guchar *cp; gchar name[256]; guint16 type, dlen, pref, weight, port; - gchar query[256]; + PurpleSrvInternalQuery query; #ifdef HAVE_SIGNAL_H purple_restore_default_signal_handlers(); #endif - if (read(in, query, 256) <= 0) { + if (read(in, &query, sizeof(query)) <= 0) { close(out); close(in); _exit(0); } - size = res_query( query, C_IN, T_SRV, (u_char*)&answer, sizeof( answer)); - + size = res_query( query.query, C_IN, query.type, (u_char*)&answer, sizeof( answer)); + qdcount = ntohs(answer.hdr.qdcount); ancount = ntohs(answer.hdr.ancount); - cp = (guchar*)&answer + sizeof(HEADER); end = (guchar*)&answer + size; @@ -138,17 +154,14 @@ size = dn_expand((unsigned char*)&answer, end, cp, name, 256); if(size < 0) goto end; - cp += size; - GETSHORT(type,cp); /* skip ttl and class since we already know it */ cp += 6; GETSHORT(dlen,cp); - - if (type == T_SRV) { + if (query.type == T_SRV) { GETSHORT(pref,cp); GETSHORT(weight,cp); @@ -168,6 +181,11 @@ srvres->weight = weight; ret = g_list_insert_sorted(ret, srvres, responsecompare); + } else if (query.type == T_TXT) { + txtres = g_new0(PurpleTxtResponse, 1); + strncpy(txtres->content, (gchar*)(++cp), dlen-1); + ret = g_list_append(ret, txtres); + cp += dlen - 1; } else { cp += dlen; } @@ -175,10 +193,12 @@ end: size = g_list_length(ret); - write(out, &size, sizeof(int)); + write(out, &(query.type), sizeof(query.type)); + write(out, &size, sizeof(size)); while (ret != NULL) { - write(out, ret->data, sizeof(PurpleSrvResponse)); + if (query.type == T_SRV) write(out, ret->data, sizeof(PurpleSrvResponse)); + if (query.type == T_TXT) write(out, ret->data, sizeof(PurpleTxtResponse)); g_free(ret->data); ret = g_list_remove(ret, ret->data); } @@ -193,39 +213,66 @@ resolved(gpointer data, gint source, PurpleInputCondition cond) { int size; + int type; PurpleSrvQueryData *query_data = (PurpleSrvQueryData*)data; - PurpleSrvResponse *res; - PurpleSrvResponse *tmp; int i; - PurpleSrvCallback cb = query_data->cb; int status; - - if (read(source, &size, sizeof(int)) == sizeof(int)) - { - ssize_t red; - purple_debug_info("dnssrv","found %d SRV entries\n", size); - tmp = res = g_new0(PurpleSrvResponse, size); - for (i = 0; i < size; i++) { - red = read(source, tmp++, sizeof(PurpleSrvResponse)); - if (red != sizeof(PurpleSrvResponse)) { - purple_debug_error("dnssrv","unable to read srv " - "response: %s\n", g_strerror(errno)); + + if (read(source, &type, sizeof(type)) == sizeof(type)) { + if (type == T_SRV) { + PurpleSrvResponse *res; + PurpleSrvResponse *tmp; + PurpleSrvCallback cb = query_data->cb.srv; + if (read(source, &size, sizeof(int)) == sizeof(int)) { + ssize_t red; + purple_debug_info("dnssrv","found %d SRV entries\n", size); + tmp = res = g_new0(PurpleSrvResponse, size); + for (i = 0; i < size; i++) { + red = read(source, tmp++, sizeof(PurpleSrvResponse)); + if (red != sizeof(PurpleSrvResponse)) { + purple_debug_error("dnssrv","unable to read srv " + "response: %s\n", g_strerror(errno)); + size = 0; + g_free(res); + res = NULL; + } + } + } else { + purple_debug_info("dnssrv","found 0 SRV entries; errno is %i\n", errno); size = 0; - g_free(res); res = NULL; } + cb(res, size, query_data->extradata); + } else if (type == T_TXT) { + PurpleTxtResponse *res; + PurpleTxtResponse *tmp; + PurpleTxtCallback cb = query_data->cb.txt; + if (read(source, &size, sizeof(int)) == sizeof(int)) { + ssize_t red; + purple_debug_info("dnssrv","found %d TXT entries\n", size); + tmp = res = g_new0(PurpleTxtResponse, size); + for (i = 0; i < size; i++) { + red = read(source, tmp++, sizeof(PurpleTxtResponse)); + if (red != sizeof(PurpleTxtResponse)) { + purple_debug_error("dnssrv","unable to read txt " + "response: %s\n", g_strerror(errno)); + size = 0; + g_free(res); + res = NULL; + } + } + } else { + purple_debug_info("dnssrv","found 0 TXT entries; errno is %i\n", errno); + size = 0; + res = NULL; + } + cb(res, size, query_data->extradata); + } else { + purple_debug_info("dnssrv","type unknown of DNS result entry; errno is %i\n", errno); } } - else - { - purple_debug_info("dnssrv","found 0 SRV entries; errno is %i\n", errno); - size = 0; - res = NULL; - } - cb(res, size, query_data->extradata); waitpid(query_data->pid, &status, 0); - purple_srv_cancel(query_data); } @@ -237,34 +284,57 @@ res_main_thread_cb(gpointer data) { PurpleSrvResponse *srvres = NULL; + PurpleTxtResponse *txtres = NULL; int size = 0; PurpleSrvQueryData *query_data = data; - if(query_data->error_message != NULL) purple_debug_error("dnssrv", query_data->error_message); else { - PurpleSrvResponse *srvres_tmp = NULL; - GSList *lst = query_data->results; + if (query_data->type == T_SRV) { + PurpleSrvResponse *srvres_tmp = NULL; + GSList *lst = query_data->results; + + size = g_slist_length(lst); - size = g_slist_length(lst); + if(query_data->cb.srv && size > 0) + srvres_tmp = srvres = g_new0(PurpleSrvResponse, size); + while (lst) { + if(query_data->cb.srv) + memcpy(srvres_tmp++, lst->data, sizeof(PurpleSrvResponse)); + g_free(lst->data); + lst = g_slist_remove(lst, lst->data); + } + + query_data->results = NULL; - if(query_data->cb && size > 0) - srvres_tmp = srvres = g_new0(PurpleSrvResponse, size); - while (lst) { - if(query_data->cb) - memcpy(srvres_tmp++, lst->data, sizeof(PurpleSrvResponse)); - g_free(lst->data); - lst = g_slist_remove(lst, lst->data); + purple_debug_info("dnssrv", "found %d SRV entries\n", size); + + if(query_data->cb.srv) query_data->cb.srv(srvres, size, query_data->extradata); + } else if (query_data->type == T_TXT) { + PurpleTxtResponse *txtres_tmp = NULL; + GSList *lst = query_data->results; + + size = g_slist_length(lst); + + if(query_data->cb.txt && size > 0) + txtres_tmp = txtres = g_new0(PurpleTxtResponse, size); + while (lst) { + if(query_data->cb.txt) + memcpy(txtres_tmp++, lst->data, sizeof(PurpleTxtResponse)); + g_free(lst->data); + lst = g_slist_remove(lst, lst->data); + } + + query_data->results = NULL; + + purple_debug_info("dnssrv", "found %d TXT entries\n", size); + + if(query_data->cb.txt) query_data->cb.txt(txtres, size, query_data->extradata); + } else { + purple_debug_error("dnssrv", "unknown query type"); } - - query_data->results = NULL; - - purple_debug_info("dnssrv", "found %d SRV entries\n", size); } - if(query_data->cb) - query_data->cb(srvres, size, query_data->extradata); - query_data->resolver = NULL; query_data->handle = 0; @@ -277,40 +347,50 @@ res_thread(gpointer data) { PDNS_RECORD dr = NULL; - int type = DNS_TYPE_SRV; + int type; DNS_STATUS ds; PurpleSrvQueryData *query_data = data; - + type = query_data->type; ds = MyDnsQuery_UTF8(query_data->query, type, DNS_QUERY_STANDARD, NULL, &dr, NULL); if (ds != ERROR_SUCCESS) { gchar *msg = g_win32_error_message(ds); - query_data->error_message = g_strdup_printf("Couldn't look up SRV record. %s (%lu).\n", msg, ds); + if (type == DNS_TYPE_SRV) { + query_data->error_message = g_strdup_printf("Couldn't look up SRV record. %s (%lu).\n", msg, ds); + } else if (type == DNS_TYPE_TXT) { + query_data->error_message = g_strdup_printf("Couldn't look up TXT record. %s (%lu).\n", msg, ds); + } g_free(msg); } else { - PDNS_RECORD dr_tmp; - GSList *lst = NULL; - DNS_SRV_DATA *srv_data; - PurpleSrvResponse *srvres; + if (type == DNS_TYPE_SRV) { + PDNS_RECORD dr_tmp; + GSList *lst = NULL; + DNS_SRV_DATA *srv_data; + PurpleSrvResponse *srvres; + + for (dr_tmp = dr; dr_tmp != NULL; dr_tmp = dr_tmp->pNext) { + /* Discard any incorrect entries. I'm not sure if this is necessary */ + if (dr_tmp->wType != type || strcmp(dr_tmp->pName, query_data->query) != 0) { + continue; + } - for (dr_tmp = dr; dr_tmp != NULL; dr_tmp = dr_tmp->pNext) { - /* Discard any incorrect entries. I'm not sure if this is necessary */ - if (dr_tmp->wType != type || strcmp(dr_tmp->pName, query_data->query) != 0) { - continue; + srv_data = &dr_tmp->Data.SRV; + srvres = g_new0(PurpleSrvResponse, 1); + strncpy(srvres->hostname, srv_data->pNameTarget, 255); + srvres->hostname[255] = '\0'; + srvres->pref = srv_data->wPriority; + srvres->port = srv_data->wPort; + srvres->weight = srv_data->wWeight; + + lst = g_slist_insert_sorted(lst, srvres, responsecompare); } - srv_data = &dr_tmp->Data.SRV; - srvres = g_new0(PurpleSrvResponse, 1); - strncpy(srvres->hostname, srv_data->pNameTarget, 255); - srvres->hostname[255] = '\0'; - srvres->pref = srv_data->wPriority; - srvres->port = srv_data->wPort; - srvres->weight = srv_data->wWeight; - - lst = g_slist_insert_sorted(lst, srvres, responsecompare); + MyDnsRecordListFree(dr, DnsFreeRecordList); + query_data->results = lst; + } else if (type == T_TXT) { + #error IMPLEMENTATION MISSING + } else { + } - - MyDnsRecordListFree(dr, DnsFreeRecordList); - query_data->results = lst; } /* back to main thread */ @@ -328,6 +408,7 @@ { char *query; PurpleSrvQueryData *query_data; + PurpleSrvInternalQuery internal_query; #ifndef _WIN32 int in[2], out[2]; int pid; @@ -375,11 +456,16 @@ close(out[1]); close(in[0]); - if (write(in[1], query, strlen(query)+1) < 0) + internal_query.type = T_SRV; + strncpy(internal_query.query, query, 255); + + if (write(in[1], &internal_query, sizeof(internal_query)) < 0) purple_debug_error("dnssrv", "Could not write to SRV resolver\n"); + query_data = g_new0(PurpleSrvQueryData, 1); - query_data->cb = cb; + query_data->type = T_SRV; + query_data->cb.srv = cb; query_data->extradata = extradata; query_data->pid = pid; query_data->fd_out = out[0]; @@ -398,7 +484,8 @@ } query_data = g_new0(PurpleSrvQueryData, 1); - query_data->cb = cb; + query_data->type = DNS_TYPE_SRV; + query_data->cb.srv = cb; query_data->query = query; query_data->extradata = extradata; @@ -422,6 +509,104 @@ #endif } +PurpleSrvQueryData *purple_txt_resolve(const char *owner, const char *domain, PurpleTxtCallback cb, gpointer extradata) +{ + char *query; + PurpleSrvQueryData *query_data; + PurpleSrvInternalQuery internal_query; +#ifndef _WIN32 + int in[2], out[2]; + int pid; +#else + GError* err = NULL; + static gboolean initialized = FALSE; +#endif + + query = g_strdup_printf("%s.%s", owner, domain); + purple_debug_info("dnssrv","querying TXT record for %s\n", query); + +#ifndef _WIN32 + if(pipe(in) || pipe(out)) { + purple_debug_error("dnssrv", "Could not create pipe\n"); + g_free(query); + cb(NULL, 0, extradata); + return NULL; + } + + pid = fork(); + if (pid == -1) { + purple_debug_error("dnssrv", "Could not create process!\n"); + cb(NULL, 0, extradata); + g_free(query); + return NULL; + } + + /* Child */ + if (pid == 0) + { + g_free(query); + + close(out[0]); + close(in[1]); + resolve(in[0], out[1]); + /* resolve() does not return */ + } + + close(out[1]); + close(in[0]); + + internal_query.type = T_TXT; + strncpy(internal_query.query, query, 255); + + if (write(in[1], &internal_query, sizeof(internal_query)) < 0) + purple_debug_error("dnssrv", "Could not write to TXT resolver\n"); + + query_data = g_new0(PurpleSrvQueryData, 1); + query_data->type = T_TXT; + query_data->cb.txt = cb; + query_data->extradata = extradata; + query_data->pid = pid; + query_data->fd_out = out[0]; + query_data->fd_in = in[1]; + query_data->handle = purple_input_add(out[0], PURPLE_INPUT_READ, resolved, query_data); + + g_free(query); + + return query_data; +#else + if (!initialized) { + MyDnsQuery_UTF8 = (void*) wpurple_find_and_loadproc("dnsapi.dll", "DnsQuery_UTF8"); + MyDnsRecordListFree = (void*) wpurple_find_and_loadproc( + "dnsapi.dll", "DnsRecordListFree"); + initialized = TRUE; + } + + query_data = g_new0(PurpleSrvQueryData, 1); + query_data->type = DNS_TYPE_TXT; + query_data->cb.txt = cb; + query_data->query = query; + query_data->extradata = extradata; + + if (!MyDnsQuery_UTF8 || !MyDnsRecordListFree) + query_data->error_message = g_strdup("System missing DNS API (Requires W2K+)\n"); + else { + query_data->resolver = g_thread_create(res_thread, query_data, FALSE, &err); + if (query_data->resolver == NULL) { + query_data->error_message = g_strdup_printf("TXT thread create failure: %s\n", (err && err->message) ? err->message : ""); + g_error_free(err); + } + } + + /* The query isn't going to happen, so finish the SRV lookup now. + * Asynchronously call the callback since stuff may not expect + * the callback to be called before this returns */ + if (query_data->error_message != NULL) + query_data->handle = purple_timeout_add(0, res_main_thread_cb, query_data); + + return query_data; +#endif +} + void purple_srv_cancel(PurpleSrvQueryData *query_data) { @@ -435,7 +620,7 @@ * just set the callback to NULL and let the DNS lookup * finish. */ - query_data->cb = NULL; + query_data->cb.srv = NULL; return; } g_free(query_data->query); @@ -446,3 +631,9 @@ #endif g_free(query_data); } + +void +purple_txt_cancel(PurpleSrvQueryData *query_data) +{ + purple_srv_cancel(query_data); +}
--- a/libpurple/dnssrv.h Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/dnssrv.h Tue Feb 17 03:39:22 2009 +0000 @@ -28,8 +28,9 @@ extern "C" { #endif +typedef struct _PurpleSrvQueryData PurpleSrvQueryData; typedef struct _PurpleSrvResponse PurpleSrvResponse; -typedef struct _PurpleSrvQueryData PurpleSrvQueryData; +typedef struct _PurpleTxtResponse PurpleTxtResponse; struct _PurpleSrvResponse { char hostname[256]; @@ -38,7 +39,12 @@ int pref; }; +struct _PurpleTxtResponse { + char content[256]; +}; + typedef void (*PurpleSrvCallback)(PurpleSrvResponse *resp, int results, gpointer data); +typedef void (*PurpleTxtCallback)(PurpleTxtResponse *resp, int results, gpointer data); /** * Queries an SRV record. @@ -58,6 +64,23 @@ */ void purple_srv_cancel(PurpleSrvQueryData *query_data); +/** + * Queries an TXT record. + * + * @param owner Name of the protocol (e.g. "_xmppconnect") + * @param domain Domain name to query (e.g. "blubb.com") + * @param cb A callback which will be called with the results + * @param extradata Extra data to be passed to the callback + */ +PurpleSrvQueryData *purple_txt_resolve(const char *owner, const char *domain, PurpleTxtCallback cb, gpointer extradata); + +/** + * Cancel an TXT DNS query. + * + * @param query_data The request to cancel. + */ +void purple_txt_cancel(PurpleSrvQueryData *query_data); + #ifdef __cplusplus } #endif
--- a/libpurple/protocols/jabber/Makefile.am Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/Makefile.am Tue Feb 17 03:39:22 2009 +0000 @@ -9,6 +9,8 @@ auth.h \ buddy.c \ buddy.h \ + bosh.c \ + bosh.h \ chat.c \ chat.h \ data.c \
--- a/libpurple/protocols/jabber/Makefile.mingw Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/Makefile.mingw Tue Feb 17 03:39:22 2009 +0000 @@ -46,6 +46,7 @@ adhoccommands.c \ auth.c \ buddy.c \ + bosh.c caps.c \ chat.c \ data.c \
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/bosh.c Tue Feb 17 03:39:22 2009 +0000 @@ -0,0 +1,636 @@ +/* + * purple - Jabber Protocol Plugin + * + * Copyright (C) 2008, Tobias Markmann <tmarkmann@googlemail.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 02111-1301 USA + * + */ +#include "internal.h" +#include "core.h" +#include "cipher.h" +#include "debug.h" +#include "prpl.h" +#include "util.h" +#include "xmlnode.h" + +#include "bosh.h" + +typedef struct _PurpleHTTPRequest PurpleHTTPRequest; +typedef struct _PurpleHTTPConnection PurpleHTTPConnection; + +typedef void (*PurpleHTTPConnectionConnectFunction)(PurpleHTTPConnection *conn); +typedef void (*PurpleHTTPConnectionDisconnectFunction)(PurpleHTTPConnection *conn); +typedef void (*PurpleBOSHConnectionConnectFunction)(PurpleBOSHConnection *conn); +typedef void (*PurpleBOSHConnectionReceiveFunction)(PurpleBOSHConnection *conn, xmlnode *node); + +static char *bosh_useragent = NULL; + +struct _PurpleBOSHConnection { + /* decoded URL */ + char *host; + int port; + char *path; + + /* Must be big enough to hold 2^53 - 1 */ + guint64 rid; + char *sid; + int wait; + + JabberStream *js; + gboolean pipelining; + PurpleHTTPConnection *conn_a; + PurpleHTTPConnection *conn_b; + + gboolean ready; + PurpleBOSHConnectionConnectFunction connect_cb; + PurpleBOSHConnectionReceiveFunction receive_cb; +}; + +struct _PurpleHTTPConnection { + int fd; + char *host; + int port; + int ie_handle; + int requests; /* number of outstanding HTTP requests */ + + GString *buf; + gboolean headers_done; + gsize handled_len; + gsize body_len; + + int pih; /* what? */ + PurpleHTTPConnectionConnectFunction connect_cb; + PurpleHTTPConnectionConnectFunction disconnect_cb; + PurpleBOSHConnection *bosh; +}; + +struct _PurpleHTTPRequest { + const char *path; + char *data; + int data_len; +}; + +static void jabber_bosh_connection_stream_restart(PurpleBOSHConnection *conn); +static gboolean jabber_bosh_connection_error_check(PurpleBOSHConnection *conn, xmlnode *node); +static void jabber_bosh_connection_received(PurpleBOSHConnection *conn, xmlnode *node); +static void jabber_bosh_connection_send_native(PurpleBOSHConnection *conn, xmlnode *node); + +static void jabber_bosh_http_connection_connect(PurpleHTTPConnection *conn); +static void jabber_bosh_http_connection_send_request(PurpleHTTPConnection *conn, PurpleHTTPRequest *req); + +void jabber_bosh_init(void) +{ + GHashTable *ui_info = purple_core_get_ui_info(); + const char *ui_name = NULL; + const char *ui_version = NULL; + + if (ui_info) { + ui_name = g_hash_table_lookup(ui_info, "name"); + ui_version = g_hash_table_lookup(ui_info, "version"); + } + + if (ui_name) + bosh_useragent = g_strdup_printf("%s%s%s (libpurple " VERSION ")", + ui_name, ui_version ? " " : "", + ui_version ? ui_version : ""); + else + bosh_useragent = g_strdup("libpurple " VERSION); +} + +void jabber_bosh_uninit(void) +{ + g_free(bosh_useragent); + bosh_useragent = NULL; +} + +static void +jabber_bosh_http_request_destroy(PurpleHTTPRequest *req) +{ + g_free(req->data); + g_free(req); +} + +static PurpleHTTPConnection* +jabber_bosh_http_connection_init(const char *host, int port) +{ + PurpleHTTPConnection *conn = g_new0(PurpleHTTPConnection, 1); + conn->host = g_strdup(host); + conn->port = port; + conn->fd = -1; + + return conn; +} + +static void +jabber_bosh_http_connection_destroy(PurpleHTTPConnection *conn) +{ + g_free(conn->host); + + if (conn->buf) + g_string_free(conn->buf, TRUE); + + if (conn->ie_handle) + purple_input_remove(conn->ie_handle); + if (conn->fd >= 0) + close(conn->fd); + + g_free(conn); +} + +PurpleBOSHConnection* +jabber_bosh_connection_init(JabberStream *js, const char *url) +{ + PurpleBOSHConnection *conn; + char *host, *path, *user, *passwd; + int port; + + if (!purple_url_parse(url, &host, &port, &path, &user, &passwd)) { + purple_debug_info("jabber", "Unable to parse given URL.\n"); + return NULL; + } + + conn = g_new0(PurpleBOSHConnection, 1); + conn->host = host; + conn->port = port; + conn->path = g_strdup_printf("/%s", path); + g_free(path); + conn->pipelining = TRUE; + + if ((user && user[0] != '\0') || (passwd && passwd[0] != '\0')) { + purple_debug_info("jabber", "Ignoring unexpected username and password " + "in BOSH URL.\n"); + } + + g_free(user); + g_free(passwd); + + conn->js = js; + /* FIXME: This doesn't seem very random */ + conn->rid = rand() % 100000 + 1728679472; + conn->ready = FALSE; + conn->conn_a = jabber_bosh_http_connection_init(conn->host, conn->port); + conn->conn_a->bosh = conn; + + return conn; +} + +void +jabber_bosh_connection_destroy(PurpleBOSHConnection *conn) +{ + g_free(conn->host); + g_free(conn->path); + + if (conn->conn_a) + jabber_bosh_http_connection_destroy(conn->conn_a); + if (conn->conn_b) + jabber_bosh_http_connection_destroy(conn->conn_b); + + g_free(conn); +} + +void jabber_bosh_connection_close(PurpleBOSHConnection *conn) +{ + xmlnode *packet = xmlnode_new("body"); + /* XEP-0124: The rid must not exceed 16 characters */ + char rid[17]; + g_snprintf(rid, sizeof(rid), "%" G_GUINT64_FORMAT, ++conn->rid); + xmlnode_set_attrib(packet, "type", "terminate"); + xmlnode_set_attrib(packet, "xmlns", "http://jabber.org/protocol/httpbind"); + xmlnode_set_attrib(packet, "sid", conn->sid); + xmlnode_set_attrib(packet, "rid", rid); + + jabber_bosh_connection_send_native(conn, packet); + xmlnode_free(packet); +} + +static void jabber_bosh_connection_stream_restart(PurpleBOSHConnection *conn) { + xmlnode *restart = xmlnode_new("body"); + /* XEP-0124: The rid must not exceed 16 characters */ + char rid[17]; + g_snprintf(rid, sizeof(rid), "%" G_GUINT64_FORMAT, ++conn->rid); + xmlnode_set_attrib(restart, "rid", rid); + xmlnode_set_attrib(restart, "sid", conn->sid); + xmlnode_set_attrib(restart, "to", conn->js->user->domain); + xmlnode_set_attrib(restart, "xml:lang", "en"); + xmlnode_set_attrib(restart, "xmpp:restart", "true"); + xmlnode_set_attrib(restart, "xmlns", "http://jabber.org/protocol/httpbind"); + xmlnode_set_attrib(restart, "xmlns:xmpp", "urn:xmpp:xbosh"); + + jabber_bosh_connection_send_native(conn, restart); + xmlnode_free(restart); +} + +static gboolean jabber_bosh_connection_error_check(PurpleBOSHConnection *conn, xmlnode *node) { + const char *type; + + type = xmlnode_get_attrib(node, "type"); + + if (type != NULL && !strcmp(type, "terminate")) { + conn->ready = FALSE; + purple_connection_error_reason (conn->js->gc, + PURPLE_CONNECTION_ERROR_OTHER_ERROR, + _("The BOSH conncetion manager suggested to terminate your session.")); + return TRUE; + } + return FALSE; +} + +static void jabber_bosh_connection_received(PurpleBOSHConnection *conn, xmlnode *node) { + xmlnode *child; + JabberStream *js = conn->js; + + g_return_if_fail(node != NULL); + if (jabber_bosh_connection_error_check(conn, node)) + return; + + child = node->child; + while (child != NULL) { + /* jabber_process_packet might free child */ + xmlnode *next = child->next; + if (child->type == XMLNODE_TYPE_TAG) { + if (!strcmp(child->name, "iq")) { + if (xmlnode_get_child(child, "session")) + conn->ready = TRUE; + } + + jabber_process_packet(js, &child); + } + + child = next; + } +} + +static void auth_response_cb(PurpleBOSHConnection *conn, xmlnode *node) { + xmlnode *child; + + g_return_if_fail(node != NULL); + if (jabber_bosh_connection_error_check(conn, node)) + return; + + child = node->child; + while(child != NULL && child->type != XMLNODE_TYPE_TAG) { + child = child->next; + } + + /* We're only expecting one XML node here, so only process the first one */ + if (child != NULL && child->type == XMLNODE_TYPE_TAG) { + JabberStream *js = conn->js; + if (!strcmp(child->name, "success")) { + jabber_bosh_connection_stream_restart(conn); + jabber_process_packet(js, &child); + conn->receive_cb = jabber_bosh_connection_received; + } else { + js->state = JABBER_STREAM_AUTHENTICATING; + jabber_process_packet(js, &child); + } + } else { + purple_debug_warning("jabber", "Received unexepcted empty BOSH packet.\n"); + } +} + +static void boot_response_cb(PurpleBOSHConnection *conn, xmlnode *node) { + const char *sid, *version; + xmlnode *packet; + + g_return_if_fail(node != NULL); + if (jabber_bosh_connection_error_check(conn, node)) + return; + + sid = xmlnode_get_attrib(node, "sid"); + version = xmlnode_get_attrib(node, "ver"); + + if (sid) { + conn->sid = g_strdup(sid); + } else { + purple_connection_error_reason(conn->js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("No session ID given")); + return; + } + + if (version) { + const char *dot = strstr(version, "."); + int major = atoi(version); + int minor = atoi(dot + 1); + + purple_debug_info("jabber", "BOSH connection manager version %s\n", version); + + if (major != 1 || minor < 6) { + purple_connection_error_reason(conn->js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Unsupported version of BOSH protocol")); + return; + } + } else { + purple_debug_info("jabber", "Missing version in BOSH initiation\n"); + } + + /* FIXME: Depending on receiving features might break with some hosts */ + packet = xmlnode_get_child(node, "features"); + conn->js->use_bosh = TRUE; + conn->receive_cb = auth_response_cb; + jabber_stream_features_parse(conn->js, packet); +} + +static void jabber_bosh_connection_boot(PurpleBOSHConnection *conn) { + xmlnode *init = xmlnode_new("body"); + /* XEP-0124: The rid must not exceed 16 characters */ + char rid[17]; + g_snprintf(rid, sizeof(rid), "%" G_GUINT64_FORMAT, ++conn->rid); + xmlnode_set_attrib(init, "content", "text/xml; charset=utf-8"); + xmlnode_set_attrib(init, "secure", "true"); +/* + xmlnode_set_attrib(init, "route", tmp = g_strdup_printf("xmpp:%s:5222", conn->js->user->domain)); + g_free(tmp); +*/ + xmlnode_set_attrib(init, "to", conn->js->user->domain); + xmlnode_set_attrib(init, "xml:lang", "en"); + xmlnode_set_attrib(init, "xmpp:version", "1.0"); + xmlnode_set_attrib(init, "ver", "1.6"); + xmlnode_set_attrib(init, "xmlns:xmpp", "urn:xmpp:xbosh"); + xmlnode_set_attrib(init, "rid", rid); + xmlnode_set_attrib(init, "wait", "60"); /* this should be adjusted automatically according to real time network behavior */ + xmlnode_set_attrib(init, "xmlns", "http://jabber.org/protocol/httpbind"); + xmlnode_set_attrib(init, "hold", "1"); + + conn->receive_cb = boot_response_cb; + jabber_bosh_connection_send_native(conn, init); + xmlnode_free(init); +} + +static void +http_received_cb(const char *data, int len, PurpleBOSHConnection *conn) +{ + if (conn->receive_cb) { + xmlnode *node = xmlnode_from_str(data, len); + if (node) { + char *txt = xmlnode_to_formatted_str(node, NULL); + printf("\nhttp_received_cb\n%s\n", txt); + g_free(txt); + conn->receive_cb(conn, node); + xmlnode_free(node); + } else { + purple_debug_warning("jabber", "BOSH: Received invalid XML\n"); + } + } else { + g_return_if_reached(); + } +} + +void jabber_bosh_connection_send(PurpleBOSHConnection *conn, xmlnode *node) { + xmlnode *packet = xmlnode_new("body"); + /* XEP-0124: The rid must not exceed 16 characters */ + char rid[17]; + g_snprintf(rid, sizeof(rid), "%" G_GUINT64_FORMAT, ++conn->rid); + xmlnode_set_attrib(packet, "xmlns", "http://jabber.org/protocol/httpbind"); + xmlnode_set_attrib(packet, "sid", conn->sid); + xmlnode_set_attrib(packet, "rid", rid); + + if (node) { + xmlnode *copy = xmlnode_copy(node); + xmlnode_insert_child(packet, copy); + if (conn->ready == TRUE) + xmlnode_set_attrib(copy, "xmlns", "jabber:client"); + } + jabber_bosh_connection_send_native(conn, packet); + xmlnode_free(packet); +} + +void jabber_bosh_connection_send_raw(PurpleBOSHConnection *conn, + const char *data, int len) +{ + xmlnode *node = xmlnode_from_str(data, len); + if (node) { + jabber_bosh_connection_send(conn, node); + xmlnode_free(node); + } else { + /* + * This best emulates what a normal XMPP server would do + * if you send bad XML. + */ + purple_connection_error_reason(conn->js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Cannot send malformed XML")); + } +} + +static void jabber_bosh_connection_send_native(PurpleBOSHConnection *conn, xmlnode *node) { + PurpleHTTPRequest *request; + + request = g_new0(PurpleHTTPRequest, 1); + request->path = conn->path; + request->data = xmlnode_to_str(node, &(request->data_len)); + + jabber_bosh_http_connection_send_request(conn->conn_a, request); +} + +static void jabber_bosh_connection_connected(PurpleHTTPConnection *conn) { + if (conn->bosh->ready == TRUE && conn->bosh->connect_cb) { + purple_debug_info("jabber", "BOSH session already exists. Trying to reuse it.\n"); + conn->bosh->receive_cb = jabber_bosh_connection_received; + conn->bosh->connect_cb(conn->bosh); + } else + jabber_bosh_connection_boot(conn->bosh); +} + +void jabber_bosh_connection_refresh(PurpleBOSHConnection *conn) +{ + jabber_bosh_connection_send(conn, NULL); +} + +static void jabber_bosh_http_connection_disconnected(PurpleHTTPConnection *conn) { + conn->connect_cb = jabber_bosh_connection_connected; + jabber_bosh_http_connection_connect(conn); +} + +void jabber_bosh_connection_connect(PurpleBOSHConnection *conn) { + conn->conn_a->connect_cb = jabber_bosh_connection_connected; + jabber_bosh_http_connection_connect(conn->conn_a); +} + +static void +jabber_bosh_http_connection_process(PurpleHTTPConnection *conn) +{ + const char *cursor; + + cursor = conn->buf->str + conn->handled_len; + + if (!conn->headers_done) { + const char *content_length = purple_strcasestr(cursor, "\r\nContent-Length"); + const char *end_of_headers = purple_strcasestr(cursor, "\r\n\r\n"); + + /* Make sure Content-Length is in headers, not body */ + if (content_length && content_length < end_of_headers) { + char *sep = strstr(content_length, ": "); + int len = atoi(sep + 2); + if (len == 0) + purple_debug_warning("jabber", "Found mangled Content-Length header.\n"); + + conn->body_len = len; + } + + if (end_of_headers) { + conn->headers_done = TRUE; + conn->handled_len = end_of_headers - conn->buf->str + 4; + cursor = end_of_headers + 4; + } else { + conn->handled_len = conn->buf->len; + return; + } + } + + /* Have we handled everything in the buffer? */ + if (conn->handled_len >= conn->buf->len) + return; + + /* Have we read all that the Content-Length promised us? */ + if (conn->buf->len - conn->handled_len < conn->body_len) + return; + + --conn->requests; + +#warning For a pure HTTP 1.1 stack, this would need to be handled elsewhere. + if (conn->bosh->ready && conn->requests == 0) { + jabber_bosh_connection_send(conn->bosh, NULL); + purple_debug_misc("jabber", "BOSH: Sending an empty request\n"); + } + + http_received_cb(conn->buf->str + conn->handled_len, conn->body_len, + conn->bosh); + + g_string_free(conn->buf, TRUE); + conn->buf = NULL; + conn->headers_done = FALSE; + conn->handled_len = conn->body_len = 0; +} + +static void +jabber_bosh_http_connection_read(gpointer data, gint fd, + PurpleInputCondition condition) +{ + PurpleHTTPConnection *conn = data; + char buffer[1025]; + int perrno; + int cnt, count = 0; + + purple_debug_info("jabber", "jabber_bosh_http_connection_read\n"); + + if (conn->buf == NULL) + conn->buf = g_string_new(""); + + while ((cnt = read(fd, buffer, sizeof(buffer))) > 0) { + purple_debug_info("jabber", "bosh read %d bytes\n", cnt); + count += cnt; + g_string_append_len(conn->buf, buffer, cnt); + } + + perrno = errno; + if (cnt == 0 && count) { + /* TODO: process should know this response ended with a closed socket + * and throw an error if it's not a complete response. */ + jabber_bosh_http_connection_process(conn); + } + + if (cnt == 0 || (cnt < 0 && perrno != EAGAIN)) { + if (cnt < 0) + purple_debug_info("jabber", "bosh read: %d\n", cnt); + else + purple_debug_info("jabber", "bosh socket closed\n"); + + purple_input_remove(conn->ie_handle); + conn->ie_handle = 0; + + if (conn->disconnect_cb) + conn->disconnect_cb(conn); + + return; + } + + jabber_bosh_http_connection_process(conn); +} + +static void jabber_bosh_http_connection_callback(gpointer data, gint source, const gchar *error) +{ + PurpleHTTPConnection *conn = data; + PurpleConnection *gc = conn->bosh->js->gc; + + if (source < 0) { + gchar *tmp; + tmp = g_strdup_printf(_("Could not establish a connection with the server:\n%s"), + error); + purple_connection_error_reason(gc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp); + g_free(tmp); + return; + } + + conn->fd = source; + + if (conn->connect_cb) + conn->connect_cb(conn); + + conn->ie_handle = purple_input_add(conn->fd, PURPLE_INPUT_READ, + jabber_bosh_http_connection_read, conn); +} + +static void jabber_bosh_http_connection_connect(PurpleHTTPConnection *conn) +{ + PurpleConnection *gc = conn->bosh->js->gc; + PurpleAccount *account = purple_connection_get_account(gc); + + if ((purple_proxy_connect(conn, account, conn->host, conn->port, jabber_bosh_http_connection_callback, conn)) == NULL) { + purple_connection_error_reason(gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Unable to create socket")); + } +} + +static void +jabber_bosh_http_connection_send_request(PurpleHTTPConnection *conn, + PurpleHTTPRequest *req) +{ + GString *packet = g_string_new(""); + int ret; + + /* TODO: Should we lie about this HTTP/1.1 support? */ + g_string_append_printf(packet, "POST %s HTTP/1.1\r\n" + "Host: %s\r\n" + "User-Agent: %s\r\n" + "Content-Encoding: text/xml; charset=utf-8\r\n" + "Content-Length: %d\r\n\r\n", + req->path, conn->host, bosh_useragent, req->data_len); + + packet = g_string_append(packet, req->data); + + purple_debug_misc("jabber", "BOSH out: %s\n", packet->str); + /* TODO: Better error handling, circbuffer or possible integration with + * low-level code in jabber.c */ + ret = write(conn->fd, packet->str, packet->len); + + ++conn->requests; + g_string_free(packet, TRUE); + jabber_bosh_http_request_destroy(req); + + if (ret < 0 && errno == EAGAIN) + purple_debug_warning("jabber", "BOSH write would have blocked\n"); + + if (ret <= 0) { + purple_connection_error_reason(conn->bosh->js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Write error")); + return; + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/bosh.h Tue Feb 17 03:39:22 2009 +0000 @@ -0,0 +1,40 @@ +/** + * @file bosh.h Buddy handlers + * + * purple + * + * Copyright (C) 2008, Tobias Markmann <tmarkmann@googlemail.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 02111-1301 USA + */ +#ifndef _PURPLE_JABBER_BOSH_H_ +#define _PURPLE_JABBER_BOSH_H_ + +typedef struct _PurpleBOSHConnection PurpleBOSHConnection; + +#include "jabber.h" + +void jabber_bosh_init(void); +void jabber_bosh_uninit(void); + +PurpleBOSHConnection* jabber_bosh_connection_init(JabberStream *js, const char *url); +void jabber_bosh_connection_destroy(PurpleBOSHConnection *conn); + +void jabber_bosh_connection_connect(PurpleBOSHConnection *conn); +void jabber_bosh_connection_close(PurpleBOSHConnection *conn); +void jabber_bosh_connection_send(PurpleBOSHConnection *conn, xmlnode *node); +void jabber_bosh_connection_send_raw(PurpleBOSHConnection *conn, const char *data, int len); +void jabber_bosh_connection_refresh(PurpleBOSHConnection *conn); +#endif /* _PURPLE_JABBER_BOSH_H_ */
--- a/libpurple/protocols/jabber/buddy.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/buddy.c Tue Feb 17 03:39:22 2009 +0000 @@ -179,9 +179,11 @@ g_free(cmd); jbr->commands = g_list_delete_link(jbr->commands, jbr->commands); } - - jabber_caps_free_clientinfo(jbr->caps); + if (jbr->caps.exts) { + g_list_foreach(jbr->caps.exts, (GFunc)g_free, NULL); + g_list_free(jbr->caps.exts); + } g_free(jbr->name); g_free(jbr->status); g_free(jbr->thread_id); @@ -493,9 +495,6 @@ void jabber_set_buddy_icon(PurpleConnection *gc, PurpleStoredImage *img) { - PurplePresence *gpresence; - PurpleStatus *status; - if(((JabberStream*)gc->proto_data)->pep) { /* XEP-0084: User Avatars */ if(img) { @@ -613,9 +612,7 @@ */ jabber_set_info(gc, purple_account_get_user_info(gc->account)); - gpresence = purple_account_get_presence(gc->account); - status = purple_presence_get_active_status(gpresence); - jabber_presence_send(gc->account, status); + jabber_presence_send(gc->proto_data, FALSE); } /* @@ -2493,23 +2490,35 @@ gboolean jabber_resource_has_capability(const JabberBuddyResource *jbr, const gchar *cap) { - const GList *iter = NULL; + const GList *node = NULL; + const JabberCapsNodeExts *exts; - if (!jbr->caps) { + if (!jbr->caps.info) { purple_debug_error("jabber", "Unable to find caps: nothing known about buddy\n"); return FALSE; } - for (iter = jbr->caps->features ; iter ; iter = g_list_next(iter)) { - if (strcmp(iter->data, cap) == 0) { - purple_debug_info("jabber", "Found cap: %s\n", (char *)iter->data); - return TRUE; + node = g_list_find_custom(jbr->caps.info->features, cap, (GCompareFunc)strcmp); + if (!node && jbr->caps.exts && jbr->caps.info->exts) { + const GList *ext; + exts = jbr->caps.info->exts; + /* Walk through all the enabled caps, checking each list for the cap. + * Don't check it twice, though. */ + for (ext = jbr->caps.exts; ext && !node; ext = ext->next) { + GList *features = g_hash_table_lookup(exts->exts, ext->data); + if (features) + node = g_list_find_custom(features, cap, (GCompareFunc)strcmp); } } - purple_debug_info("jabber", "Cap %s not found\n", cap); - return FALSE; + /* TODO: Are these messages actually useful? */ + if (node) + purple_debug_info("jabber", "Found cap: %s\n", cap); + else + purple_debug_info("jabber", "Cap %s not found\n", cap); + + return (node != NULL); } gboolean
--- a/libpurple/protocols/jabber/buddy.h Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/buddy.h Tue Feb 17 03:39:22 2009 +0000 @@ -81,7 +81,10 @@ char *name; char *os; } client; - JabberCapsClientInfo *caps; + struct { + JabberCapsClientInfo *info; + GList *exts; + } caps; GList *commands; } JabberBuddyResource;
--- a/libpurple/protocols/jabber/caps.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/caps.c Tue Feb 17 03:39:22 2009 +0000 @@ -21,99 +21,222 @@ #include "internal.h" +#include "debug.h" #include "caps.h" -#include <string.h> -#include "internal.h" +#include "cipher.h" +#include "iq.h" +#include "presence.h" #include "util.h" -#include "iq.h" #define JABBER_CAPS_FILENAME "xmpp-caps.xml" -static GHashTable *capstable = NULL; /* JabberCapsKey -> JabberCapsValue */ +typedef struct _JabberDataFormField { + gchar *var; + GList *values; +} JabberDataFormField; typedef struct _JabberCapsKey { char *node; char *ver; + char *hash; } JabberCapsKey; -typedef struct _JabberCapsValueExt { - GList *identities; /* JabberCapsIdentity */ - GList *features; /* char * */ -} JabberCapsValueExt; +static GHashTable *capstable = NULL; /* JabberCapsKey -> JabberCapsClientInfo */ +static GHashTable *nodetable = NULL; /* char *node -> JabberCapsNodeExts */ +static guint save_timer = 0; + +/** + * Processes a query-node and returns a JabberCapsClientInfo object with all relevant info. + * + * @param query A query object. + * @return A JabberCapsClientInfo object. + */ +static JabberCapsClientInfo *jabber_caps_parse_client_info(xmlnode *query); + +/* Free a GList of allocated char* */ +static void +free_string_glist(GList *list) +{ + g_list_foreach(list, (GFunc)g_free, NULL); + g_list_free(list); +} + +static JabberCapsNodeExts* +jabber_caps_node_exts_ref(JabberCapsNodeExts *exts) +{ + g_return_val_if_fail(exts != NULL, NULL); -typedef struct _JabberCapsValue { - GList *identities; /* JabberCapsIdentity */ - GList *features; /* char * */ - GHashTable *ext; /* char * -> JabberCapsValueExt */ -} JabberCapsValue; + ++exts->ref; + return exts; +} + +static void +jabber_caps_node_exts_unref(JabberCapsNodeExts *exts) +{ + if (exts == NULL) + return; + + g_return_if_fail(exts->ref != 0); -static guint jabber_caps_hash(gconstpointer key) { - const JabberCapsKey *name = key; - guint nodehash = g_str_hash(name->node); - guint verhash = g_str_hash(name->ver); - - return nodehash ^ verhash; + if (--exts->ref != 0) + return; + + g_hash_table_destroy(exts->exts); + g_free(exts); +} + +static guint jabber_caps_hash(gconstpointer data) { + const JabberCapsKey *key = data; + guint nodehash = g_str_hash(key->node); + guint verhash = g_str_hash(key->ver); + /* 'hash' was optional in XEP-0115 v1.4 and I think g_str_hash crashes on + * NULL >:O. Okay, maybe I've played too much Zelda, but that looks like + * a Deku Shrub... */ + guint hashhash = (key->hash ? g_str_hash(key->hash) : 0); + return nodehash ^ verhash ^ hashhash; } static gboolean jabber_caps_compare(gconstpointer v1, gconstpointer v2) { const JabberCapsKey *name1 = v1; const JabberCapsKey *name2 = v2; + /* Again, hash might be NULL and I *know* strcmp will crash on NULL. */ + gboolean hasheq = ((!name1->hash && !name2->hash) || + (name1->hash && name2->hash && !strcmp(name1->hash, name2->hash))); - return strcmp(name1->node,name2->node) == 0 && strcmp(name1->ver,name2->ver) == 0; + return strcmp(name1->node, name2->node) == 0 && + strcmp(name1->ver, name2->ver) == 0 && + hasheq; } -static void jabber_caps_destroy_key(gpointer key) { - JabberCapsKey *keystruct = key; - g_free(keystruct->node); - g_free(keystruct->ver); - g_free(keystruct); +void jabber_caps_destroy_key(gpointer data) { + JabberCapsKey *key = data; + g_free(key->node); + g_free(key->ver); + g_free(key->hash); + g_free(key); } -static void jabber_caps_destroy_value(gpointer value) { - JabberCapsValue *valuestruct = value; - while(valuestruct->identities) { - JabberCapsIdentity *id = valuestruct->identities->data; +static void +jabber_caps_client_info_destroy(JabberCapsClientInfo *info) +{ + if (info == NULL) + return; + + while(info->identities) { + JabberIdentity *id = info->identities->data; g_free(id->category); g_free(id->type); g_free(id->name); + g_free(id->lang); g_free(id); - - valuestruct->identities = g_list_delete_link(valuestruct->identities,valuestruct->identities); + info->identities = g_list_delete_link(info->identities, info->identities); } - while(valuestruct->features) { - g_free(valuestruct->features->data); - valuestruct->features = g_list_delete_link(valuestruct->features,valuestruct->features); + + free_string_glist(info->features); + free_string_glist(info->forms); + + jabber_caps_node_exts_unref(info->exts); + + g_free(info); +} + +/* NOTE: Takes a reference to the exts, unref it if you don't really want to + * keep it around. */ +static JabberCapsNodeExts* +jabber_caps_find_exts_by_node(const char *node) +{ + JabberCapsNodeExts *exts; + if (NULL == (exts = g_hash_table_lookup(nodetable, node))) { + exts = g_new0(JabberCapsNodeExts, 1); + exts->exts = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, + (GDestroyNotify)free_string_glist); + g_hash_table_insert(nodetable, g_strdup(node), jabber_caps_node_exts_ref(exts)); } - g_hash_table_destroy(valuestruct->ext); - g_free(valuestruct); + + return jabber_caps_node_exts_ref(exts); +} + +static void +exts_to_xmlnode(gconstpointer key, gconstpointer value, gpointer user_data) +{ + const char *identifier = key; + const GList *features = value, *node; + xmlnode *client = user_data, *ext, *feature; + + ext = xmlnode_new_child(client, "ext"); + xmlnode_set_attrib(ext, "identifier", identifier); + + for (node = features; node; node = node->next) { + feature = xmlnode_new_child(ext, "feature"); + xmlnode_set_attrib(feature, "var", (const gchar *)node->data); + } } -static void jabber_caps_ext_destroy_value(gpointer value) { - JabberCapsValueExt *valuestruct = value; - while(valuestruct->identities) { - JabberCapsIdentity *id = valuestruct->identities->data; - g_free(id->category); - g_free(id->type); - g_free(id->name); - g_free(id); - - valuestruct->identities = g_list_delete_link(valuestruct->identities,valuestruct->identities); +static void jabber_caps_store_client(gpointer key, gpointer value, gpointer user_data) { + JabberCapsKey *clientinfo = key; + JabberCapsClientInfo *props = value; + xmlnode *root = user_data; + xmlnode *client = xmlnode_new_child(root, "client"); + GList *iter; + + xmlnode_set_attrib(client, "node", clientinfo->node); + xmlnode_set_attrib(client, "ver", clientinfo->ver); + if (clientinfo->hash) + xmlnode_set_attrib(client, "hash", clientinfo->hash); + for(iter = props->identities; iter; iter = g_list_next(iter)) { + JabberIdentity *id = iter->data; + xmlnode *identity = xmlnode_new_child(client, "identity"); + xmlnode_set_attrib(identity, "category", id->category); + xmlnode_set_attrib(identity, "type", id->type); + if (id->name) + xmlnode_set_attrib(identity, "name", id->name); + if (id->lang) + xmlnode_set_attrib(identity, "lang", id->lang); } - while(valuestruct->features) { - g_free(valuestruct->features->data); - valuestruct->features = g_list_delete_link(valuestruct->features,valuestruct->features); + + for(iter = props->features; iter; iter = g_list_next(iter)) { + const char *feat = iter->data; + xmlnode *feature = xmlnode_new_child(client, "feature"); + xmlnode_set_attrib(feature, "var", feat); } - g_free(valuestruct); + + for(iter = props->forms; iter; iter = g_list_next(iter)) { + /* FIXME: See #7814 */ + xmlnode *xdata = iter->data; + xmlnode_insert_child(client, xmlnode_copy(xdata)); + } + + /* TODO: Ideally, only save this once-per-node... */ + if (props->exts) + g_hash_table_foreach(props->exts->exts, (GHFunc)exts_to_xmlnode, client); } -static void jabber_caps_load(void); +static gboolean +do_jabber_caps_store(gpointer data) +{ + char *str; + int length = 0; + xmlnode *root = xmlnode_new("capabilities"); + g_hash_table_foreach(capstable, jabber_caps_store_client, root); + str = xmlnode_to_formatted_str(root, &length); + xmlnode_free(root); + purple_util_write_data_to_file(JABBER_CAPS_FILENAME, str, length); + g_free(str); -void jabber_caps_init(void) { - capstable = g_hash_table_new_full(jabber_caps_hash, jabber_caps_compare, jabber_caps_destroy_key, jabber_caps_destroy_value); - jabber_caps_load(); + save_timer = 0; + return FALSE; } -static void jabber_caps_load(void) { +static void +schedule_caps_save(void) +{ + if (save_timer == 0) + save_timer = purple_timeout_add_seconds(5, do_jabber_caps_store, NULL); +} + +static void +jabber_caps_load(void) +{ xmlnode *capsdata = purple_util_read_xml_from_file(JABBER_CAPS_FILENAME, "XMPP capabilities cache"); xmlnode *client; @@ -130,11 +253,17 @@ continue; if(!strcmp(client->name, "client")) { JabberCapsKey *key = g_new0(JabberCapsKey, 1); - JabberCapsValue *value = g_new0(JabberCapsValue, 1); + JabberCapsClientInfo *value = g_new0(JabberCapsClientInfo, 1); xmlnode *child; + JabberCapsNodeExts *exts = NULL; key->node = g_strdup(xmlnode_get_attrib(client,"node")); key->ver = g_strdup(xmlnode_get_attrib(client,"ver")); - value->ext = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, jabber_caps_ext_destroy_value); + key->hash = g_strdup(xmlnode_get_attrib(client,"hash")); + + /* v1.3 capabilities */ + if (key->hash == NULL) + exts = jabber_caps_find_exts_by_node(key->node); + for(child = client->child; child; child = child->next) { if(child->type != XMLNODE_TYPE_TAG) continue; @@ -147,432 +276,654 @@ const char *category = xmlnode_get_attrib(child, "category"); const char *type = xmlnode_get_attrib(child, "type"); const char *name = xmlnode_get_attrib(child, "name"); - - JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); + const char *lang = xmlnode_get_attrib(child, "lang"); + JabberIdentity *id; + + if (!category || !type) + continue; + + id = g_new0(JabberIdentity, 1); id->category = g_strdup(category); id->type = g_strdup(type); id->name = g_strdup(name); + id->lang = g_strdup(lang); value->identities = g_list_append(value->identities,id); - } else if(!strcmp(child->name,"ext")) { + } else if(!strcmp(child->name,"x")) { + /* FIXME: See #7814 -- this will cause problems if anyone + * ever actually specifies forms. In fact, for this to + * work properly, that bug needs to be fixed in + * xmlnode_from_str, not the output version... */ + value->forms = g_list_append(value->forms, xmlnode_copy(child)); + } else if (!strcmp(child->name, "ext") && key->hash != NULL) { + purple_debug_warning("jabber", "Ignoring exts when reading new-style caps\n"); + } else if (!strcmp(child->name, "ext")) { + /* TODO: Do we care about reading in the identities listed here? */ const char *identifier = xmlnode_get_attrib(child, "identifier"); - if(identifier) { - xmlnode *extchild; - - JabberCapsValueExt *extvalue = g_new0(JabberCapsValueExt, 1); - - for(extchild = child->child; extchild; extchild = extchild->next) { - if(extchild->type != XMLNODE_TYPE_TAG) + xmlnode *node; + GList *features = NULL; + + if (!identifier) + continue; + + for (node = child->child; node; node = node->next) { + if (node->type != XMLNODE_TYPE_TAG) + continue; + if (!strcmp(node->name, "feature")) { + const char *var = xmlnode_get_attrib(node, "var"); + if (!var) continue; - if(!strcmp(extchild->name,"feature")) { - const char *var = xmlnode_get_attrib(extchild, "var"); - if(!var) - continue; - extvalue->features = g_list_append(extvalue->features,g_strdup(var)); - } else if(!strcmp(extchild->name,"identity")) { - const char *category = xmlnode_get_attrib(extchild, "category"); - const char *type = xmlnode_get_attrib(extchild, "type"); - const char *name = xmlnode_get_attrib(extchild, "name"); - - JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); - id->category = g_strdup(category); - id->type = g_strdup(type); - id->name = g_strdup(name); - - extvalue->identities = g_list_append(extvalue->identities,id); - } + features = g_list_prepend(features, g_strdup(var)); } - g_hash_table_replace(value->ext, g_strdup(identifier), extvalue); } + + if (features) { + g_hash_table_insert(exts->exts, g_strdup(identifier), + features); + } else + purple_debug_warning("jabber", "Caps ext %s had no features.\n", + identifier); } } + + value->exts = exts; g_hash_table_replace(capstable, key, value); + } } xmlnode_free(capsdata); } -static void jabber_caps_store_ext(gpointer key, gpointer value, gpointer user_data) { - const char *extname = key; - JabberCapsValueExt *props = value; - xmlnode *root = user_data; - xmlnode *ext = xmlnode_new_child(root,"ext"); - GList *iter; - - xmlnode_set_attrib(ext,"identifier",extname); - - for(iter = props->identities; iter; iter = g_list_next(iter)) { - JabberCapsIdentity *id = iter->data; - xmlnode *identity = xmlnode_new_child(ext, "identity"); - xmlnode_set_attrib(identity, "category", id->category); - xmlnode_set_attrib(identity, "type", id->type); - if (id->name) - xmlnode_set_attrib(identity, "name", id->name); - } - - for(iter = props->features; iter; iter = g_list_next(iter)) { - const char *feat = iter->data; - xmlnode *feature = xmlnode_new_child(ext, "feature"); - xmlnode_set_attrib(feature, "var", feat); - } -} - -static void jabber_caps_store_client(gpointer key, gpointer value, gpointer user_data) { - JabberCapsKey *clientinfo = key; - JabberCapsValue *props = value; - xmlnode *root = user_data; - xmlnode *client = xmlnode_new_child(root,"client"); - GList *iter; - - xmlnode_set_attrib(client,"node",clientinfo->node); - xmlnode_set_attrib(client,"ver",clientinfo->ver); - - for(iter = props->identities; iter; iter = g_list_next(iter)) { - JabberCapsIdentity *id = iter->data; - xmlnode *identity = xmlnode_new_child(client, "identity"); - xmlnode_set_attrib(identity, "category", id->category); - xmlnode_set_attrib(identity, "type", id->type); - if (id->name) - xmlnode_set_attrib(identity, "name", id->name); - } - - for(iter = props->features; iter; iter = g_list_next(iter)) { - const char *feat = iter->data; - xmlnode *feature = xmlnode_new_child(client, "feature"); - xmlnode_set_attrib(feature, "var", feat); - } - - g_hash_table_foreach(props->ext,jabber_caps_store_ext,client); -} - -static void jabber_caps_store(void) { - char *str; - xmlnode *root = xmlnode_new("capabilities"); - g_hash_table_foreach(capstable, jabber_caps_store_client, root); - str = xmlnode_to_formatted_str(root, NULL); - xmlnode_free(root); - purple_util_write_data_to_file(JABBER_CAPS_FILENAME, str, -1); - g_free(str); +void jabber_caps_init(void) +{ + nodetable = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)jabber_caps_node_exts_unref); + capstable = g_hash_table_new_full(jabber_caps_hash, jabber_caps_compare, jabber_caps_destroy_key, (GDestroyNotify)jabber_caps_client_info_destroy); + jabber_caps_load(); } -/* this function assumes that all information is available locally */ -static JabberCapsClientInfo *jabber_caps_collect_info(const char *node, const char *ver, GList *ext) { - JabberCapsClientInfo *result; - JabberCapsKey *key = g_new0(JabberCapsKey, 1); - JabberCapsValue *caps; - GList *iter; - - key->node = (char *)node; - key->ver = (char *)ver; - - caps = g_hash_table_lookup(capstable,key); - - g_free(key); - - if (caps == NULL) - return NULL; - - result = g_new0(JabberCapsClientInfo, 1); - - /* join all information */ - for(iter = caps->identities; iter; iter = g_list_next(iter)) { - JabberCapsIdentity *id = iter->data; - JabberCapsIdentity *newid = g_new0(JabberCapsIdentity, 1); - newid->category = g_strdup(id->category); - newid->type = g_strdup(id->type); - newid->name = g_strdup(id->name); - - result->identities = g_list_append(result->identities,newid); - } - for(iter = caps->features; iter; iter = g_list_next(iter)) { - const char *feat = iter->data; - char *newfeat = g_strdup(feat); - - result->features = g_list_append(result->features,newfeat); +void jabber_caps_uninit(void) +{ + if (save_timer != 0) { + purple_timeout_remove(save_timer); + save_timer = 0; + do_jabber_caps_store(NULL); } - - for(iter = ext; iter; iter = g_list_next(iter)) { - const char *extname = iter->data; - JabberCapsValueExt *extinfo = g_hash_table_lookup(caps->ext,extname); - - if(extinfo) { - GList *iter2; - for(iter2 = extinfo->identities; iter2; iter2 = g_list_next(iter2)) { - JabberCapsIdentity *id = iter2->data; - JabberCapsIdentity *newid = g_new0(JabberCapsIdentity, 1); - newid->category = g_strdup(id->category); - newid->type = g_strdup(id->type); - newid->name = g_strdup(id->name); - - result->identities = g_list_append(result->identities,newid); - } - for(iter2 = extinfo->features; iter2; iter2 = g_list_next(iter2)) { - const char *feat = iter2->data; - char *newfeat = g_strdup(feat); - - result->features = g_list_append(result->features,newfeat); - } - } - } - return result; -} - -void jabber_caps_free_clientinfo(JabberCapsClientInfo *clientinfo) { - if(!clientinfo) - return; - while(clientinfo->identities) { - JabberCapsIdentity *id = clientinfo->identities->data; - g_free(id->category); - g_free(id->type); - g_free(id->name); - g_free(id); - - clientinfo->identities = g_list_delete_link(clientinfo->identities,clientinfo->identities); - } - while(clientinfo->features) { - char *feat = clientinfo->features->data; - g_free(feat); - - clientinfo->features = g_list_delete_link(clientinfo->features,clientinfo->features); - } - - g_free(clientinfo); + g_hash_table_destroy(capstable); + g_hash_table_destroy(nodetable); + capstable = NULL; } typedef struct _jabber_caps_cbplususerdata { + guint ref; + jabber_caps_get_info_cb cb; - gpointer user_data; - + gpointer cb_data; + char *who; char *node; char *ver; - GList *ext; - unsigned extOutstanding; + char *hash; + + JabberCapsClientInfo *info; + + GList *exts; + guint extOutstanding; + JabberCapsNodeExts *node_exts; } jabber_caps_cbplususerdata; -typedef struct jabber_ext_userdata { - jabber_caps_cbplususerdata *userdata; - char *node; -} jabber_ext_userdata; +static jabber_caps_cbplususerdata* +cbplususerdata_ref(jabber_caps_cbplususerdata *data) +{ + g_return_val_if_fail(data != NULL, NULL); -static void jabber_caps_get_info_check_completion(jabber_caps_cbplususerdata *userdata) { - if(userdata->extOutstanding == 0) { - userdata->cb(jabber_caps_collect_info(userdata->node, userdata->ver, userdata->ext), userdata->user_data); - g_free(userdata->who); - g_free(userdata->node); - g_free(userdata->ver); - while(userdata->ext) { - g_free(userdata->ext->data); - userdata->ext = g_list_delete_link(userdata->ext,userdata->ext); - } - g_free(userdata); - } + ++data->ref; + return data; } -static void jabber_caps_ext_iqcb(JabberStream *js, xmlnode *packet, gpointer data) { - /* collect data and fetch all exts */ - xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", "http://jabber.org/protocol/disco#info"); - jabber_ext_userdata *extuserdata = data; - jabber_caps_cbplususerdata *userdata = extuserdata->userdata; - const char *node = extuserdata->node; - - --userdata->extOutstanding; - - /* TODO: Better error handling */ +static void +cbplususerdata_unref(jabber_caps_cbplususerdata *data) +{ + if (data == NULL) + return; - if(node && query) { - const char *key; - JabberCapsValue *client; - xmlnode *child; - JabberCapsValueExt *value = g_new0(JabberCapsValueExt, 1); - JabberCapsKey *clientkey = g_new0(JabberCapsKey, 1); + g_return_if_fail(data->ref != 0); - clientkey->node = userdata->node; - clientkey->ver = userdata->ver; - - client = g_hash_table_lookup(capstable, clientkey); - - g_free(clientkey); + if (--data->ref > 0) + return; - /* split node by #, key either points to \0 or the correct ext afterwards */ - for(key = node; key[0] != '\0'; ++key) { - if(key[0] == '#') { - ++key; - break; - } - } + g_free(data->who); + g_free(data->node); + g_free(data->ver); + g_free(data->hash); - for(child = query->child; child; child = child->next) { - if(child->type != XMLNODE_TYPE_TAG) - continue; - if(!strcmp(child->name,"feature")) { - const char *var = xmlnode_get_attrib(child, "var"); - if(!var) - continue; - value->features = g_list_append(value->features,g_strdup(var)); - } else if(!strcmp(child->name,"identity")) { - const char *category = xmlnode_get_attrib(child, "category"); - const char *type = xmlnode_get_attrib(child, "type"); - const char *name = xmlnode_get_attrib(child, "name"); - - JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); - id->category = g_strdup(category); - id->type = g_strdup(type); - id->name = g_strdup(name); - - value->identities = g_list_append(value->identities,id); - } - } - g_hash_table_replace(client->ext, g_strdup(key), value); - - jabber_caps_store(); - } - - g_free(extuserdata->node); - g_free(extuserdata); - jabber_caps_get_info_check_completion(userdata); + /* If we have info here, it's already in the capstable, so don't free it */ + if (data->exts) + free_string_glist(data->exts); + if (data->node_exts) + jabber_caps_node_exts_unref(data->node_exts); + g_free(data); } -static void jabber_caps_client_iqcb(JabberStream *js, xmlnode *packet, gpointer data) { - /* collect data and fetch all exts */ +static void +jabber_caps_get_info_complete(jabber_caps_cbplususerdata *userdata) +{ + userdata->cb(userdata->info, userdata->exts, userdata->cb_data); + userdata->info = NULL; + userdata->exts = NULL; + + if (userdata->ref != 1) + purple_debug_warning("jabber", "Lost a reference to caps cbdata: %d\n", + userdata->ref); +} + +static void +jabber_caps_client_iqcb(JabberStream *js, xmlnode *packet, gpointer data) +{ xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", "http://jabber.org/protocol/disco#info"); jabber_caps_cbplususerdata *userdata = data; - - /* TODO: Better error checking! */ - - if (query) { - JabberCapsValue *value = g_new0(JabberCapsValue, 1); - JabberCapsKey *key = g_new0(JabberCapsKey, 1); - xmlnode *child; - GList *iter; - - value->ext = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, jabber_caps_ext_destroy_value); + JabberCapsClientInfo *info = NULL, *value; + const char *type = xmlnode_get_attrib(packet, "type"); + JabberCapsKey key; - key->node = g_strdup(userdata->node); - key->ver = g_strdup(userdata->ver); + if (!query || !strcmp(type, "error")) { + /* Any outstanding exts will be dealt with via ref-counting */ + userdata->cb(NULL, NULL, userdata->cb_data); + cbplususerdata_unref(userdata); + return; + } - for(child = query->child; child; child = child->next) { - if(child->type != XMLNODE_TYPE_TAG) - continue; - if(!strcmp(child->name,"feature")) { - const char *var = xmlnode_get_attrib(child, "var"); - if(!var) - continue; - value->features = g_list_append(value->features, g_strdup(var)); - } else if(!strcmp(child->name,"identity")) { - const char *category = xmlnode_get_attrib(child, "category"); - const char *type = xmlnode_get_attrib(child, "type"); - const char *name = xmlnode_get_attrib(child, "name"); + /* check hash */ + info = jabber_caps_parse_client_info(query); - JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); - id->category = g_strdup(category); - id->type = g_strdup(type); - id->name = g_strdup(name); - - value->identities = g_list_append(value->identities,id); - } + /* Only validate if these are v1.5 capabilities */ + if (userdata->hash) { + gchar *hash = NULL; + if (!strcmp(userdata->hash, "sha-1")) { + hash = jabber_caps_calculate_hash(info, "sha1"); + } else if (!strcmp(userdata->hash, "md5")) { + hash = jabber_caps_calculate_hash(info, "md5"); } - g_hash_table_replace(capstable, key, value); - jabber_caps_store(); - - /* fetch all exts */ - for(iter = userdata->ext; iter; iter = g_list_next(iter)) { - JabberIq *iq = jabber_iq_new_query(js, JABBER_IQ_GET, - "http://jabber.org/protocol/disco#info"); - xmlnode *query = xmlnode_get_child_with_namespace(iq->node, - "query", "http://jabber.org/protocol/disco#info"); - char *node = g_strdup_printf("%s#%s", userdata->node, (const char*)iter->data); - jabber_ext_userdata *ext_data = g_new0(jabber_ext_userdata, 1); - ext_data->node = node; - ext_data->userdata = userdata; + if (!hash || strcmp(hash, userdata->ver)) { + purple_debug_warning("jabber", "Could not validate caps info from %s\n", + xmlnode_get_attrib(packet, "from")); - xmlnode_set_attrib(query, "node", node); - xmlnode_set_attrib(iq->node, "to", userdata->who); - - jabber_iq_set_callback(iq, jabber_caps_ext_iqcb, ext_data); - jabber_iq_send(iq); + userdata->cb(NULL, NULL, userdata->cb_data); + jabber_caps_client_info_destroy(info); + cbplususerdata_unref(userdata); + g_free(hash); + return; } - } else - /* Don't wait for the ext discoveries; they aren't going to happen */ - userdata->extOutstanding = 0; + g_free(hash); + } + + if (!userdata->hash && userdata->node_exts) { + /* If the ClientInfo doesn't have information about the exts, give them + * ours (along with our ref) */ + info->exts = userdata->node_exts; + userdata->node_exts = NULL; + } + + key.node = userdata->node; + key.ver = userdata->ver; + key.hash = userdata->hash; - jabber_caps_get_info_check_completion(userdata); + /* Use the copy of this data already in the table if it exists or insert + * a new one if we need to */ + if ((value = g_hash_table_lookup(capstable, &key))) { + jabber_caps_client_info_destroy(info); + info = value; + } else { + JabberCapsKey *n_key = g_new(JabberCapsKey, 1); + n_key->node = userdata->node; + n_key->ver = userdata->ver; + n_key->hash = userdata->hash; + userdata->node = userdata->ver = userdata->hash = NULL; + + /* The capstable gets a reference */ + g_hash_table_insert(capstable, n_key, info); + schedule_caps_save(); + } + + userdata->info = info; + + if (userdata->extOutstanding == 0) + jabber_caps_get_info_complete(userdata); + + cbplususerdata_unref(userdata); } -void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, const char *ver, const char *ext, jabber_caps_get_info_cb cb, gpointer user_data) { - JabberCapsValue *client; - JabberCapsKey *key = g_new0(JabberCapsKey, 1); - char *originalext = g_strdup(ext); - jabber_caps_cbplususerdata *userdata = g_new0(jabber_caps_cbplususerdata, 1); +typedef struct { + const char *name; + jabber_caps_cbplususerdata *data; +} ext_iq_data; + +static void +jabber_caps_ext_iqcb(JabberStream *js, xmlnode *packet, gpointer data) +{ + xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", + "http://jabber.org/protocol/disco#info"); + xmlnode *child; + ext_iq_data *userdata = data; + const char *type = xmlnode_get_attrib(packet, "type"); + GList *features = NULL; + JabberCapsNodeExts *node_exts; + + if (!query || !strcmp(type, "error")) { + cbplususerdata_unref(userdata->data); + g_free(userdata); + return; + } + + /* So, we decrement this after checking for an error, which means that + * if there *is* an error, we'll never call the callback passed to + * jabber_caps_get_info. We will still free all of our data, though. + */ + --userdata->data->extOutstanding; + + for (child = xmlnode_get_child(query, "feature"); child; + child = xmlnode_get_next_twin(child)) { + const char *var = xmlnode_get_attrib(child, "var"); + if (var) + features = g_list_prepend(features, g_strdup(var)); + } + + node_exts = (userdata->data->info ? userdata->data->info->exts : + userdata->data->node_exts); + g_hash_table_insert(node_exts->exts, g_strdup(userdata->name), features); + schedule_caps_save(); + + /* Are we done? */ + if (userdata->data->info && userdata->data->extOutstanding == 0) + jabber_caps_get_info_complete(userdata->data); + + cbplususerdata_unref(userdata->data); + g_free(userdata); +} + +void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, + const char *ver, const char *hash, const char *ext, + jabber_caps_get_info_cb cb, gpointer user_data) +{ + JabberCapsClientInfo *info; + JabberCapsKey key; + jabber_caps_cbplususerdata *userdata; + + if (ext && *ext && hash) + purple_debug_warning("jabber", "Ignoring exts in new-style caps from %s\n", + who); + + /* Using this in a read-only fashion, so the cast is OK */ + key.node = (char *)node; + key.ver = (char *)ver; + key.hash = (char *)hash; + + info = g_hash_table_lookup(capstable, &key); + if (info && hash) { + /* v1.5 - We already have all the information we care about */ + cb(info, NULL, user_data); + return; + } + + userdata = g_new0(jabber_caps_cbplususerdata, 1); + /* This ref is given to fetching the basic node#ver info if we need it + * or unrefed at the bottom of this function */ + cbplususerdata_ref(userdata); userdata->cb = cb; - userdata->user_data = user_data; + userdata->cb_data = user_data; userdata->who = g_strdup(who); userdata->node = g_strdup(node); userdata->ver = g_strdup(ver); - - if(originalext) { - int i; - gchar **splat = g_strsplit(originalext, " ", 0); - for(i =0; splat[i]; i++) { - userdata->ext = g_list_append(userdata->ext, splat[i]); - ++userdata->extOutstanding; - } - g_free(splat); - } - g_free(originalext); + userdata->hash = g_strdup(hash); - key->node = (char *)node; - key->ver = (char *)ver; - - client = g_hash_table_lookup(capstable, key); + if (info) { + userdata->info = info; + } else { + /* If we don't have the basic information about the client, we need + * to fetch it. */ + JabberIq *iq; + xmlnode *query; + char *nodever; - g_free(key); - - if(!client) { - JabberIq *iq = jabber_iq_new_query(js,JABBER_IQ_GET,"http://jabber.org/protocol/disco#info"); - xmlnode *query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#info"); - char *nodever = g_strdup_printf("%s#%s", node, ver); + iq = jabber_iq_new_query(js, JABBER_IQ_GET, + "http://jabber.org/protocol/disco#info"); + query = xmlnode_get_child_with_namespace(iq->node, "query", + "http://jabber.org/protocol/disco#info"); + nodever = g_strdup_printf("%s#%s", node, ver); xmlnode_set_attrib(query, "node", nodever); g_free(nodever); xmlnode_set_attrib(iq->node, "to", who); - jabber_iq_set_callback(iq,jabber_caps_client_iqcb,userdata); + jabber_iq_set_callback(iq, jabber_caps_client_iqcb, userdata); jabber_iq_send(iq); - } else { - GList *iter; - /* fetch unknown exts only */ - for(iter = userdata->ext; iter; iter = g_list_next(iter)) { - JabberCapsValueExt *extvalue = g_hash_table_lookup(client->ext, (const char*)iter->data); - JabberIq *iq; - xmlnode *query; - char *nodever; - jabber_ext_userdata *ext_data; + } + + /* Are there any exts that we don't recognize? */ + if (ext && *ext && !hash) { + JabberCapsNodeExts *node_exts; + gchar **splat = g_strsplit(ext, " ", 0); + int i; + + if (info) { + if (info->exts) + node_exts = info->exts; + else + node_exts = info->exts = jabber_caps_find_exts_by_node(node); + } else + /* We'll put it in later once we have the client info */ + node_exts = userdata->node_exts = jabber_caps_find_exts_by_node(node); - if(extvalue) { - /* we already have this ext, don't bother with it */ - --userdata->extOutstanding; - continue; + for (i = 0; splat[i]; ++i) { + userdata->exts = g_list_prepend(userdata->exts, splat[i]); + /* Look it up if we don't already know what it means */ + if (!g_hash_table_lookup(node_exts->exts, splat[i])) { + JabberIq *iq; + xmlnode *query; + char *nodeext; + ext_iq_data *cbdata = g_new(ext_iq_data, 1); + + cbdata->name = splat[i]; + cbdata->data = cbplususerdata_ref(userdata); + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, + "http://jabber.org/protocol/disco#info"); + query = xmlnode_get_child_with_namespace(iq->node, "query", + "http://jabber.org/protocol/disco#info"); + nodeext = g_strdup_printf("%s#%s", node, splat[i]); + xmlnode_set_attrib(query, "node", nodeext); + g_free(nodeext); + xmlnode_set_attrib(iq->node, "to", who); + + jabber_iq_set_callback(iq, jabber_caps_ext_iqcb, cbdata); + jabber_iq_send(iq); + + ++userdata->extOutstanding; } - - ext_data = g_new0(jabber_ext_userdata, 1); + splat[i] = NULL; + } + /* All the strings are now part of the GList, so don't need + * g_strfreev. */ + g_free(splat); + } - iq = jabber_iq_new_query(js,JABBER_IQ_GET,"http://jabber.org/protocol/disco#info"); - query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#info"); - nodever = g_strdup_printf("%s#%s", node, (const char*)iter->data); - xmlnode_set_attrib(query, "node", nodever); - xmlnode_set_attrib(iq->node, "to", who); + if (userdata->info && userdata->extOutstanding == 0) { + jabber_caps_get_info_complete(userdata); + cbplususerdata_unref(userdata); + } +} - ext_data->node = nodever; - ext_data->userdata = userdata; +static gint +jabber_identity_compare(gconstpointer a, gconstpointer b) +{ + const JabberIdentity *ac; + const JabberIdentity *bc; + gint cat_cmp; + gint typ_cmp; + + ac = a; + bc = b; - jabber_iq_set_callback(iq, jabber_caps_ext_iqcb, ext_data); - jabber_iq_send(iq); + if ((cat_cmp = strcmp(ac->category, bc->category)) == 0) { + if ((typ_cmp = strcmp(ac->type, bc->type)) == 0) { + if (!ac->lang && !bc->lang) { + return 0; + } else if (ac->lang && !bc->lang) { + return 1; + } else if (!ac->lang && bc->lang) { + return -1; + } else { + return strcmp(ac->lang, bc->lang); + } + } else { + return typ_cmp; } - /* maybe we have all data available anyways? This is the ideal case where no network traffic is necessary */ - jabber_caps_get_info_check_completion(userdata); + } else { + return cat_cmp; } } +static gchar *jabber_caps_get_formtype(const xmlnode *x) { + xmlnode *formtypefield; + formtypefield = xmlnode_get_child(x, "field"); + while (formtypefield && strcmp(xmlnode_get_attrib(formtypefield, "var"), "FORM_TYPE")) formtypefield = xmlnode_get_next_twin(formtypefield); + formtypefield = xmlnode_get_child(formtypefield, "value"); + return xmlnode_get_data(formtypefield);; +} + +static gint +jabber_xdata_compare(gconstpointer a, gconstpointer b) +{ + const xmlnode *aformtypefield = a; + const xmlnode *bformtypefield = b; + char *aformtype; + char *bformtype; + int result; + + aformtype = jabber_caps_get_formtype(aformtypefield); + bformtype = jabber_caps_get_formtype(bformtypefield); + + result = strcmp(aformtype, bformtype); + g_free(aformtype); + g_free(bformtype); + return result; +} + +static JabberCapsClientInfo *jabber_caps_parse_client_info(xmlnode *query) +{ + xmlnode *child; + JabberCapsClientInfo *info; + + if (!query || strcmp(query->xmlns, "http://jabber.org/protocol/disco#info")) + return 0; + + info = g_new0(JabberCapsClientInfo, 1); + + for(child = query->child; child; child = child->next) { + if (!strcmp(child->name,"identity")) { + /* parse identity */ + const char *category = xmlnode_get_attrib(child, "category"); + const char *type = xmlnode_get_attrib(child, "type"); + const char *name = xmlnode_get_attrib(child, "name"); + const char *lang = xmlnode_get_attrib(child, "lang"); + JabberIdentity *id; + + if (!category || !type) + continue; + + id = g_new0(JabberIdentity, 1); + id->category = g_strdup(category); + id->type = g_strdup(type); + id->name = g_strdup(name); + id->lang = g_strdup(lang); + + info->identities = g_list_append(info->identities, id); + } else if (!strcmp(child->name, "feature")) { + /* parse feature */ + const char *var = xmlnode_get_attrib(child, "var"); + if (var) + info->features = g_list_prepend(info->features, g_strdup(var)); + } else if (!strcmp(child->name, "x")) { + if (child->xmlns && !strcmp(child->xmlns, "jabber:x:data")) { + /* x-data form */ + xmlnode *dataform = xmlnode_copy(child); + info->forms = g_list_append(info->forms, dataform); + } + } + } + return info; +} + +static gint jabber_caps_xdata_field_compare(gconstpointer a, gconstpointer b) +{ + const JabberDataFormField *ac = a; + const JabberDataFormField *bc = b; + + return strcmp(ac->var, bc->var); +} + +static GList* jabber_caps_xdata_get_fields(const xmlnode *x) +{ + GList *fields = NULL; + xmlnode *field; + + if (!x) + return NULL; + + for (field = xmlnode_get_child(x, "field"); field; field = xmlnode_get_next_twin(field)) { + xmlnode *value; + JabberDataFormField *xdatafield = g_new0(JabberDataFormField, 1); + xdatafield->var = g_strdup(xmlnode_get_attrib(field, "var")); + + for (value = xmlnode_get_child(field, "value"); value; value = xmlnode_get_next_twin(value)) { + gchar *val = xmlnode_get_data(value); + xdatafield->values = g_list_append(xdatafield->values, val); + } + + xdatafield->values = g_list_sort(xdatafield->values, (GCompareFunc)strcmp); + fields = g_list_append(fields, xdatafield); + } + + fields = g_list_sort(fields, jabber_caps_xdata_field_compare); + return fields; +} + +static GString* +jabber_caps_verification_append(GString *verification, const gchar *string) +{ + verification = g_string_append(verification, string); + return g_string_append_c(verification, '<'); +} + +gchar *jabber_caps_calculate_hash(JabberCapsClientInfo *info, const char *hash) +{ + GList *node; + GString *verification; + PurpleCipherContext *context; + guint8 checksum[20]; + gsize checksum_size = 20; + gboolean success; + + if (!info || !(context = purple_cipher_context_new_by_name(hash, NULL))) + return NULL; + + /* sort identities, features and x-data forms */ + info->identities = g_list_sort(info->identities, jabber_identity_compare); + info->features = g_list_sort(info->features, (GCompareFunc)strcmp); + info->forms = g_list_sort(info->forms, jabber_xdata_compare); + + verification = g_string_new(""); + + /* concat identities to the verification string */ + for (node = info->identities; node; node = node->next) { + JabberIdentity *id = (JabberIdentity*)node->data; + + g_string_append_printf(verification, "%s/%s/%s/%s<", id->category, + id->type, id->lang ? id->lang : "", id->name); + } + + /* concat features to the verification string */ + for (node = info->features; node; node = node->next) { + verification = jabber_caps_verification_append(verification, node->data); + } + + /* concat x-data forms to the verification string */ + for(node = info->forms; node; node = node->next) { + xmlnode *data = (xmlnode *)node->data; + gchar *formtype = jabber_caps_get_formtype(data); + GList *fields = jabber_caps_xdata_get_fields(data); + + /* append FORM_TYPE's field value to the verification string */ + verification = jabber_caps_verification_append(verification, formtype); + g_free(formtype); + + while (fields) { + GList *value; + JabberDataFormField *field = (JabberDataFormField*)fields->data; + + if (strcmp(field->var, "FORM_TYPE")) { + /* Append the "var" attribute */ + verification = jabber_caps_verification_append(verification, field->var); + /* Append <value/> elements' cdata */ + for(value = field->values; value; value = value->next) { + verification = jabber_caps_verification_append(verification, value->data); + g_free(value->data); + } + } + + g_free(field->var); + g_list_free(field->values); + + fields = g_list_delete_link(fields, fields); + } + } + + /* generate hash */ + purple_cipher_context_append(context, (guchar*)verification->str, verification->len); + + success = purple_cipher_context_digest(context, verification->len, + checksum, &checksum_size); + + g_string_free(verification, TRUE); + purple_cipher_context_destroy(context); + + return (success ? purple_base64_encode(checksum, checksum_size) : NULL); +} + +void jabber_caps_calculate_own_hash(JabberStream *js) { + JabberCapsClientInfo info; + GList *iter = 0; + GList *features = 0; + + if (!jabber_identities && !jabber_features) { + /* This really shouldn't ever happen */ + purple_debug_warning("jabber", "No features or identities, cannot calculate own caps hash.\n"); + g_free(js->caps_hash); + js->caps_hash = NULL; + return; + } + + /* build the currently-supported list of features */ + if (jabber_features) { + for (iter = jabber_features; iter; iter = iter->next) { + JabberFeature *feat = iter->data; + if(!feat->is_enabled || feat->is_enabled(js, feat->namespace)) { + features = g_list_append(features, feat->namespace); + } + } + } + + info.features = features; + info.identities = jabber_identities; + info.forms = NULL; + + g_free(js->caps_hash); + js->caps_hash = jabber_caps_calculate_hash(&info, "sha1"); + g_list_free(features); +} + +const gchar* jabber_caps_get_own_hash(JabberStream *js) +{ + if (!js->caps_hash) + jabber_caps_calculate_own_hash(js); + + return js->caps_hash; +} + +void jabber_caps_broadcast_change() +{ + GList *node, *accounts = purple_accounts_get_all_active(); + + for (node = accounts; node; node = node->next) { + PurpleAccount *account = node->data; + const char *prpl_id = purple_account_get_protocol_id(account); + if (!strcmp("prpl-jabber", prpl_id) && purple_account_is_connected(account)) { + PurpleConnection *gc = purple_account_get_connection(account); + jabber_presence_send(gc->proto_data, TRUE); + } + } + + g_list_free(accounts); +} +
--- a/libpurple/protocols/jabber/caps.h Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/caps.h Tue Feb 17 03:39:22 2009 +0000 @@ -26,24 +26,77 @@ #include "jabber.h" -/* Implementation of XEP-0115 */ +/* Implementation of XEP-0115 - Entity Capabilities */ -typedef struct _JabberCapsIdentity { - char *category; - char *type; - char *name; -} JabberCapsIdentity; +typedef struct _JabberCapsNodeExts JabberCapsNodeExts; struct _JabberCapsClientInfo { - GList *identities; /* JabberCapsIdentity */ + GList *identities; /* JabberIdentity */ GList *features; /* char * */ + GList *forms; /* xmlnode * */ + JabberCapsNodeExts *exts; +}; + +/* + * This stores a set of exts "known" for a specific node (which indicates + * a specific client -- for reference, Pidgin, Finch, Meebo, et al share one + * node.) In XEP-0115 v1.3, exts are used for features that may or may not be + * present at a given time (PEP things, buzz might be disabled, etc). + * + * This structure is shared among all JabberCapsClientInfo instances matching + * a specific node (if the capstable key->hash == NULL, which indicates that + * the ClientInfo is using v1.3 caps as opposed to v1.5 caps). + * + * It's only exposed so that jabber_resource_has_capability can use it. + * Everyone else, STAY AWAY! + */ +struct _JabberCapsNodeExts { + guint ref; + GHashTable *exts; /* char *ext_name -> GList *features */ }; -typedef void (*jabber_caps_get_info_cb)(JabberCapsClientInfo *info, gpointer user_data); +typedef void (*jabber_caps_get_info_cb)(JabberCapsClientInfo *info, GList *exts, gpointer user_data); void jabber_caps_init(void); +void jabber_caps_uninit(void); -void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, const char *ver, const char *ext, jabber_caps_get_info_cb cb, gpointer user_data); -void jabber_caps_free_clientinfo(JabberCapsClientInfo *clientinfo); +void jabber_caps_destroy_key(gpointer value); + +/** + * Main entity capabilites function to get the capabilities of a contact. + * + * The callback will be called synchronously if we already have the + * capabilities for the specified (node,ver,hash) (and, if exts are specified, + * if we know what each means) + */ +void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, + const char *ver, const char *hash, + const char *ext, jabber_caps_get_info_cb cb, + gpointer user_data); + +/** + * Takes a JabberCapsClientInfo pointer and returns the caps hash according to + * XEP-0115 Version 1.5. + * + * @param info A JabberCapsClientInfo pointer. + * @param hash Hash cipher to be used. Either sha-1 or md5. + * @return The base64 encoded SHA-1 hash; must be freed by caller + */ +gchar *jabber_caps_calculate_hash(JabberCapsClientInfo *info, const char *hash); + +/** + * Calculate SHA1 hash for own featureset. + */ +void jabber_caps_calculate_own_hash(JabberStream *js); + +/** Get the current caps hash. + * @ret hash +**/ +const gchar* jabber_caps_get_own_hash(JabberStream *js); + +/** + * Broadcast a new calculated hash using a <presence> stanza. + */ +void jabber_caps_broadcast_change(void); #endif /* _PURPLE_JABBER_CAPS_H_ */
--- a/libpurple/protocols/jabber/data.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/data.c Tue Feb 17 03:39:22 2009 +0000 @@ -244,4 +244,5 @@ g_hash_table_destroy(local_data_by_alt); g_hash_table_destroy(local_data_by_cid); g_hash_table_destroy(remote_data_by_cid); + local_data_by_alt = local_data_by_cid = remote_data_by_cid = NULL; }
--- a/libpurple/protocols/jabber/disco.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/disco.c Tue Feb 17 03:39:22 2009 +0000 @@ -88,7 +88,6 @@ void jabber_disco_info_parse(JabberStream *js, xmlnode *packet) { const char *from = xmlnode_get_attrib(packet, "from"); const char *type = xmlnode_get_attrib(packet, "type"); - if(!from || !type) return; @@ -98,6 +97,10 @@ xmlnode *in_query; const char *node = NULL; + char *node_uri = NULL; + + /* create custom caps node URI */ + node_uri = g_strconcat(CAPS0115_NODE, "#", jabber_caps_get_own_hash(js), NULL); if((in_query = xmlnode_get_child(packet, "query"))) { node = xmlnode_get_attrib(in_query, "node"); @@ -115,90 +118,40 @@ if(node) xmlnode_set_attrib(query, "node", node); - if(!node || !strcmp(node, CAPS0115_NODE "#" VERSION)) { - identity = xmlnode_new_child(query, "identity"); - xmlnode_set_attrib(identity, "category", "client"); - xmlnode_set_attrib(identity, "type", "pc"); /* XXX: bot, console, - * handheld, pc, phone, - * web */ - xmlnode_set_attrib(identity, "name", PACKAGE); - SUPPORT_FEATURE("jabber:iq:last") - SUPPORT_FEATURE("jabber:iq:oob") - SUPPORT_FEATURE("jabber:iq:time") - SUPPORT_FEATURE("xmpp:urn:time") - SUPPORT_FEATURE("jabber:iq:version") - SUPPORT_FEATURE("jabber:x:conference") - 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/muc") - SUPPORT_FEATURE("http://jabber.org/protocol/muc#user") - SUPPORT_FEATURE("http://jabber.org/protocol/si") - SUPPORT_FEATURE("http://jabber.org/protocol/si/profile/file-transfer") - SUPPORT_FEATURE("http://jabber.org/protocol/xhtml-im") - SUPPORT_FEATURE("urn:xmpp:ping") - SUPPORT_FEATURE("http://www.xmpp.org/extensions/xep-0199.html#ns") - - if(!node) { /* non-caps disco#info, add all enabled extensions */ - GList *features; - for(features = jabber_features; features; features = features->next) { - JabberFeature *feat = (JabberFeature*)features->data; - if(feat->is_enabled == NULL || feat->is_enabled(js, feat->shortname, feat->namespace) == TRUE) - SUPPORT_FEATURE(feat->namespace); - } + if(!node || !strcmp(node, node_uri)) { + GList *features, *identities; + for(identities = jabber_identities; identities; identities = identities->next) { + JabberIdentity *ident = (JabberIdentity*)identities->data; + identity = xmlnode_new_child(query, "identity"); + xmlnode_set_attrib(identity, "category", ident->category); + xmlnode_set_attrib(identity, "type", ident->type); + if (ident->lang) + xmlnode_set_attrib(identity, "xml:lang", ident->lang); + if (ident->name) + xmlnode_set_attrib(identity, "name", ident->name); + } + for(features = jabber_features; features; features = features->next) { + JabberFeature *feat = (JabberFeature*)features->data; + if (!feat->is_enabled || feat->is_enabled(js, feat->namespace)) { + feature = xmlnode_new_child(query, "feature"); + xmlnode_set_attrib(feature, "var", feat->namespace); + } } } else { - const char *ext = NULL; - unsigned pos; - unsigned nodelen = strlen(node); - unsigned capslen = strlen(CAPS0115_NODE); - /* do a basic plausability check */ - if(nodelen > capslen+1) { - /* verify that the string is CAPS0115#<ext> and get the pointer to the ext part */ - for(pos = 0; pos < capslen+1; ++pos) { - if(pos == capslen) { - if(node[pos] == '#') - ext = &node[pos+1]; - else - break; - } else if(node[pos] != CAPS0115_NODE[pos]) - break; - } + xmlnode *error, *inf; - if(ext != NULL) { - /* look for that ext */ - GList *features; - for(features = jabber_features; features; features = features->next) { - JabberFeature *feat = (JabberFeature*)features->data; - if(!strcmp(feat->shortname, ext)) { - SUPPORT_FEATURE(feat->namespace); - break; - } - } - if(features == NULL) - ext = NULL; - } - } + /* XXX: gross hack, implement jabber_iq_set_type or something */ + xmlnode_set_attrib(iq->node, "type", "error"); + iq->type = JABBER_IQ_ERROR; - if(ext == NULL) { - xmlnode *error, *inf; - - /* XXX: gross hack, implement jabber_iq_set_type or something */ - xmlnode_set_attrib(iq->node, "type", "error"); - iq->type = JABBER_IQ_ERROR; - - error = xmlnode_new_child(query, "error"); - xmlnode_set_attrib(error, "code", "404"); - xmlnode_set_attrib(error, "type", "cancel"); - inf = xmlnode_new_child(error, "item-not-found"); - xmlnode_set_namespace(inf, "urn:ietf:params:xml:ns:xmpp-stanzas"); - } + error = xmlnode_new_child(query, "error"); + xmlnode_set_attrib(error, "code", "404"); + xmlnode_set_attrib(error, "type", "cancel"); + inf = xmlnode_new_child(error, "item-not-found"); + xmlnode_set_namespace(inf, "urn:ietf:params:xml:ns:xmpp-stanzas"); } - + g_free(node_uri); jabber_iq_send(iq); } else if(!strcmp(type, "result")) { xmlnode *query = xmlnode_get_child(packet, "query"); @@ -347,9 +300,6 @@ jabber_roster_request(js); } - /* Send initial presence; this will trigger receipt of presence for contacts on the roster */ - jabber_presence_send(js->gc->account, NULL); - if (js->server_caps & JABBER_CAP_ADHOC) { /* The server supports ad-hoc commands, so let's request the list */ jabber_adhoc_server_get_list(js);
--- a/libpurple/protocols/jabber/jabber.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/jabber.c Tue Feb 17 03:39:22 2009 +0000 @@ -40,6 +40,7 @@ #include "version.h" #include "xmlnode.h" +#include "caps.h" #include "auth.h" #include "buddy.h" #include "chat.h" @@ -63,7 +64,9 @@ #define JABBER_CONNECT_STEPS (js->gsc ? 9 : 5) static PurplePlugin *my_protocol = NULL; + GList *jabber_features = NULL; +GList *jabber_identities = NULL; static void jabber_unregister_account_cb(JabberStream *js); static void try_srv_connect(JabberStream *js); @@ -89,7 +92,7 @@ { const char *type = xmlnode_get_attrib(packet, "type"); if(type && !strcmp(type, "result")) { - jabber_stream_set_state(js, JABBER_STREAM_CONNECTED); + jabber_disco_items_server(js); if(js->unregistration) jabber_unregister_account_cb(js); } else { @@ -180,7 +183,7 @@ return purple_strreplace(input, "__HOSTNAME__", hostname); } -static void jabber_stream_features_parse(JabberStream *js, xmlnode *packet) +void jabber_stream_features_parse(JabberStream *js, xmlnode *packet) { if(xmlnode_get_child(packet, "starttls")) { if(jabber_process_starttls(js, packet)) @@ -420,7 +423,13 @@ } #endif - do_jabber_send_raw(js, data, len); + if (len == -1) + len = strlen(data); + + if (js->use_bosh) + jabber_bosh_connection_send_raw(js->bosh, data, len); + else + do_jabber_send_raw(js, data, len); } int jabber_prpl_send_raw(PurpleConnection *gc, const char *buf, int len) @@ -441,9 +450,13 @@ if(NULL == packet) return; - txt = xmlnode_to_str(packet, &len); - jabber_send_raw(js, txt, len); - g_free(txt); + if (js->use_bosh) + jabber_bosh_connection_send(js->bosh, packet); + else { + txt = xmlnode_to_str(packet, &len); + jabber_send_raw(js, txt, len); + g_free(txt); + } } static void jabber_pong_cb(JabberStream *js, xmlnode *packet, gpointer unused) @@ -583,6 +596,47 @@ jabber_stream_set_state(js, JABBER_STREAM_INITIALIZING_ENCRYPTION); } +static void +jabber_bosh_login_callback(PurpleBOSHConnection *conn) +{ + purple_debug_info("jabber","YAY...BOSH connection established.\n"); +} + +static void +txt_resolved_cb(PurpleTxtResponse *resp, int results, gpointer data) +{ + JabberStream *js = data; + int n; + + js->srv_query_data = NULL; + + if (results == 0) { + gchar *tmp; + tmp = g_strdup_printf(_("Could not find alternative XMPP connection methods after failing to connect directly.\n")); + purple_connection_error_reason (js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp); + g_free(tmp); + return; + } + + for (n = 0; n < results; n++) { + gchar **token; + token = g_strsplit(resp[n].content, "=", 2); + if (!strcmp(token[0], "_xmpp-client-xbosh")) { + purple_debug_info("jabber","Found alternative connection method using %s at %s.\n", token[0], token[1]); + js->bosh = jabber_bosh_connection_init(js, token[1]); + js->use_bosh = TRUE; + g_strfreev(token); + break; + } + g_strfreev(token); + } + if (js->bosh) { + jabber_bosh_connection_connect(js->bosh); + } else { + purple_debug_info("jabber","Didn't find an alternative connection method.\n"); + } +} static void jabber_login_callback(gpointer data, gint source, const gchar *error) @@ -595,12 +649,8 @@ purple_debug_error("jabber", "Unable to connect to server: %s. Trying next SRV record.\n", error); try_srv_connect(js); } else { - gchar *tmp; - tmp = g_strdup_printf(_("Could not establish a connection with the server:\n%s"), - error); - purple_connection_error_reason(gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp); - g_free(tmp); + purple_debug_info("jabber","Couldn't connect directly to %s. Trying to find alternative connection methods, like BOSH.\n", js->user->domain); + js->srv_query_data = purple_txt_resolve("_xmppconnect", js->user->domain, txt_resolved_cb, js); } return; } @@ -709,6 +759,8 @@ "connect_server", ""); JabberStream *js; JabberBuddy *my_jb = NULL; + /* XXX FORCE_BOSH */ + gboolean force_bosh = purple_account_get_bool(account, "force_bosh", FALSE); gc->flags |= PURPLE_CONNECTION_HTML | PURPLE_CONNECTION_ALLOW_CUSTOM_SMILEY; @@ -749,6 +801,12 @@ jabber_stream_set_state(js, JABBER_STREAM_CONNECTING); + /* XXX FORCE_BOSH: Remove this */ + if (force_bosh) { + js->srv_query_data = purple_txt_resolve("_xmppconnect", js->user->domain, txt_resolved_cb, js); + return; + } + /* if they've got old-ssl mode going, we probably want to ignore SRV lookups */ if(purple_account_get_bool(js->gc->account, "old_ssl", FALSE)) { if(purple_ssl_is_supported()) { @@ -766,7 +824,7 @@ /* no old-ssl, so if they've specified a connect server, we'll use that, otherwise we'll * invoke the magic of SRV lookups, to figure out host and port */ if(!js->gsc) { - if(connect_server[0]) { + if(connect_server[0]) { jabber_login_connect(js, js->user->domain, connect_server, purple_account_get_int(account, "port", 5222), TRUE); } else { js->srv_query_data = purple_srv_resolve("xmpp-client", @@ -1324,8 +1382,12 @@ * if we were forcibly disconnected because it will crash * on some SSL backends. */ - if (!gc->disconnect_timeout) - jabber_send_raw(js, "</stream:stream>", -1); + if (!gc->disconnect_timeout) { + if (js->use_bosh) + jabber_bosh_connection_close(js->bosh); + else + jabber_send_raw(js, "</stream:stream>", -1); + } if (js->srv_query_data) purple_srv_cancel(js->srv_query_data); @@ -1341,6 +1403,9 @@ close(js->fd); } + if (js->bosh) + jabber_bosh_connection_destroy(js->bosh); + jabber_buddy_remove_all_pending_buddy_info_requests(js); jabber_parser_free(js); @@ -1382,6 +1447,7 @@ if(js->user) jabber_id_free(js->user); g_free(js->avatar_hash); + g_free(js->caps_hash); purple_circ_buffer_destroy(js->write_buffer); if(js->writeh) @@ -1474,7 +1540,6 @@ case JABBER_STREAM_CONNECTED: /* now we can alert the core that we're ready to send status */ purple_connection_set_state(js->gc, PURPLE_CONNECTED); - jabber_disco_items_server(js); break; } } @@ -1592,31 +1657,27 @@ jabber_iq_send(iq); } -void jabber_add_feature(const char *shortname, const char *namespace, JabberFeatureEnabled cb) { +void jabber_add_feature(const char *namespace, JabberFeatureEnabled cb) { JabberFeature *feat; - g_return_if_fail(shortname != NULL); g_return_if_fail(namespace != NULL); feat = g_new0(JabberFeature,1); - feat->shortname = g_strdup(shortname); feat->namespace = g_strdup(namespace); feat->is_enabled = cb; /* try to remove just in case it already exists in the list */ - jabber_remove_feature(shortname); + jabber_remove_feature(namespace); jabber_features = g_list_append(jabber_features, feat); } -void jabber_remove_feature(const char *shortname) { +void jabber_remove_feature(const char *namespace) { GList *feature; for(feature = jabber_features; feature; feature = feature->next) { JabberFeature *feat = (JabberFeature*)feature->data; - if(!strcmp(feat->shortname, shortname)) { - g_free(feat->shortname); + if(!strcmp(feat->namespace, namespace)) { g_free(feat->namespace); - g_free(feature->data); jabber_features = g_list_delete_link(jabber_features, feature); break; @@ -1624,6 +1685,53 @@ } } +static void jabber_features_destroy(void) +{ + while (jabber_features) { + JabberFeature *feature = jabber_features->data; + g_free(feature->namespace); + g_free(feature); + jabber_features = g_list_remove_link(jabber_features, jabber_features); + } +} + +void jabber_add_identity(const gchar *category, const gchar *type, const gchar *lang, const gchar *name) { + GList *identity; + JabberIdentity *ident; + /* both required according to XEP-0030 */ + g_return_if_fail(category != NULL); + g_return_if_fail(type != NULL); + + for(identity = jabber_identities; identity; identity = identity->next) { + JabberIdentity *ident = (JabberIdentity*)identity->data; + if (!strcmp(ident->category, category) && + !strcmp(ident->type, type) && + ((!ident->lang && !lang) || (ident->lang && lang && !strcmp(ident->lang, lang)))) { + return; + } + } + + ident = g_new0(JabberIdentity, 1); + ident->category = g_strdup(category); + ident->type = g_strdup(type); + ident->lang = g_strdup(lang); + ident->name = g_strdup(name); + jabber_identities = g_list_append(jabber_identities, ident); +} + +static void jabber_identities_destroy(void) +{ + while (jabber_identities) { + JabberIdentity *id = jabber_identities->data; + g_free(id->category); + g_free(id->type); + g_free(id->lang); + g_free(id->name); + g_free(id); + jabber_identities = g_list_remove_link(jabber_identities, jabber_identities); + } +} + const char *jabber_list_icon(PurpleAccount *a, PurpleBuddy *b) { return "jabber"; @@ -2449,7 +2557,6 @@ JabberBuddy *jb; JabberBuddyResource *jbr; - GList *iter; if(!username) return FALSE; @@ -2466,31 +2573,30 @@ return FALSE; } - if(!jbr->caps) { + /* Is this message sufficiently useful to not just fold it in with the tail error condition below? */ + if(!jbr->caps.info) { *error = g_strdup_printf(_("Unable to buzz, because there is nothing known about user %s."), username); return FALSE; } - for(iter = jbr->caps->features; iter; iter = g_list_next(iter)) { - if(!strcmp(iter->data, "http://www.xmpp.org/extensions/xep-0224.html#ns")) { - xmlnode *buzz, *msg = xmlnode_new("message"); - gchar *to; - - to = g_strdup_printf("%s/%s", username, jbr->name); - xmlnode_set_attrib(msg, "to", to); - g_free(to); - - /* avoid offline storage */ - xmlnode_set_attrib(msg, "type", "headline"); - - buzz = xmlnode_new_child(msg, "attention"); - xmlnode_set_namespace(buzz, "http://www.xmpp.org/extensions/xep-0224.html#ns"); - - jabber_send(js, msg); - xmlnode_free(msg); - - return TRUE; - } + if (jabber_resource_has_capability(jbr, "http://www.xmpp.org/extensions/xep-0224.html#ns")) { + xmlnode *buzz, *msg = xmlnode_new("message"); + gchar *to; + + to = g_strdup_printf("%s/%s", username, jbr->name); + xmlnode_set_attrib(msg, "to", to); + g_free(to); + + /* avoid offline storage */ + xmlnode_set_attrib(msg, "type", "headline"); + + buzz = xmlnode_new_child(msg, "attention"); + xmlnode_set_namespace(buzz, "http://www.xmpp.org/extensions/xep-0224.html#ns"); + + jabber_send(js, msg); + xmlnode_free(msg); + + return TRUE; } *error = g_strdup_printf(_("Unable to buzz, because the user %s does not support it."), username); @@ -2624,8 +2730,99 @@ _("buzz: Buzz a user to get their attention"), NULL); } +/* IPC functions */ + +/** + * IPC function for determining if a contact supports a certain feature. + * + * @param account The PurpleAccount + * @param jid The full JID of the contact. + * @param feature The feature's namespace. + * + * @return TRUE if supports feature; else FALSE. + */ +static gboolean +jabber_ipc_contact_has_feature(PurpleAccount *account, const gchar *jid, + const gchar *feature) +{ + PurpleConnection *gc = purple_account_get_connection(account); + JabberStream *js; + JabberBuddy *jb; + JabberBuddyResource *jbr; + gchar *resource; + + if (!purple_account_is_connected(account)) + return FALSE; + js = gc->proto_data; + + if (!(resource = jabber_get_resource(jid)) || + !(jb = jabber_buddy_find(js, jid, FALSE)) || + !(jbr = jabber_buddy_find_resource(jb, resource))) { + g_free(resource); + return FALSE; + } + + g_free(resource); + + return jabber_resource_has_capability(jbr, feature); +} + +static void +jabber_ipc_add_feature(const gchar *feature) +{ + if (!feature) + return; + jabber_add_feature(feature, 0); + + /* send presence with new caps info for all connected accounts */ + jabber_caps_broadcast_change(); +} + void jabber_init_plugin(PurplePlugin *plugin) { - my_protocol = plugin; + my_protocol = plugin; + + jabber_add_identity("client", "pc", NULL, PACKAGE); + + /* initialize jabber_features list */ + jabber_add_feature("jabber:iq:last", 0); + jabber_add_feature("jabber:iq:oob", 0); + jabber_add_feature("jabber:iq:time", 0); + jabber_add_feature("urn:xmpp:time", 0); + jabber_add_feature("jabber:iq:version", 0); + jabber_add_feature("jabber:x:conference", 0); + jabber_add_feature("http://jabber.org/protocol/bytestreams", 0); + jabber_add_feature("http://jabber.org/protocol/caps", 0); + jabber_add_feature("http://jabber.org/protocol/chatstates", 0); + jabber_add_feature("http://jabber.org/protocol/disco#info", 0); + jabber_add_feature("http://jabber.org/protocol/disco#items", 0); +#if 0 + jabber_add_feature("http://jabber.org/protocol/ibb", 0); +#endif + jabber_add_feature("http://jabber.org/protocol/muc", 0); + jabber_add_feature("http://jabber.org/protocol/muc#user", 0); + jabber_add_feature("http://jabber.org/protocol/si", 0); + jabber_add_feature("http://jabber.org/protocol/si/profile/file-transfer", 0); + jabber_add_feature("http://jabber.org/protocol/xhtml-im", 0); + jabber_add_feature("urn:xmpp:ping", 0); + + /* IPC functions */ + purple_plugin_ipc_register(plugin, "contact_has_feature", PURPLE_CALLBACK(jabber_ipc_contact_has_feature), + purple_marshal_BOOLEAN__POINTER_POINTER_POINTER, + purple_value_new(PURPLE_TYPE_BOOLEAN), 3, + purple_value_new(PURPLE_TYPE_SUBTYPE, PURPLE_SUBTYPE_ACCOUNT), + purple_value_new(PURPLE_TYPE_STRING), + purple_value_new(PURPLE_TYPE_STRING)); + purple_plugin_ipc_register(plugin, "add_feature", PURPLE_CALLBACK(jabber_ipc_add_feature), + purple_marshal_VOID__POINTER, + NULL, 1, + purple_value_new(PURPLE_TYPE_STRING)); } + +void +jabber_uninit_plugin(void) +{ + jabber_features_destroy(); + jabber_identities_destroy(); +}
--- a/libpurple/protocols/jabber/jabber.h Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/jabber.h Tue Feb 17 03:39:22 2009 +0000 @@ -60,12 +60,13 @@ #include "jutil.h" #include "xmlnode.h" #include "buddy.h" +#include "bosh.h" #ifdef HAVE_CYRUS_SASL #include <sasl/sasl.h> #endif -#define CAPS0115_NODE "http://pidgin.im/caps" +#define CAPS0115_NODE "http://pidgin.im/" /* Index into attention_types list */ #define JABBER_BUZZ 0 @@ -206,6 +207,9 @@ gboolean vcard_fetched; + /* Entity Capabilities hash */ + char *caps_hash; + /* does the local server support PEP? */ gboolean pep; @@ -233,10 +237,15 @@ /* A purple timeout tag for the keepalive */ int keepalive_timeout; - + PurpleSrvResponse *srv_rec; guint srv_rec_idx; guint max_srv_rec_idx; + + /* BOSH stuff */ + gboolean use_bosh; + PurpleBOSHConnection *bosh; + /** * This linked list contains PurpleUtilFetchUrlData structs * for when we lookup buddy icons from a url @@ -244,15 +253,22 @@ GSList *url_datas; }; -typedef gboolean (JabberFeatureEnabled)(JabberStream *js, const gchar *shortname, const gchar *namespace); +typedef gboolean (JabberFeatureEnabled)(JabberStream *js, const gchar *namespace); typedef struct _JabberFeature { - gchar *shortname; gchar *namespace; JabberFeatureEnabled *is_enabled; } JabberFeature; +typedef struct _JabberIdentity +{ + gchar *category; + gchar *type; + gchar *name; + gchar *lang; +} JabberIdentity; + typedef struct _JabberBytestreamsStreamhost { char *jid; char *host; @@ -262,7 +278,9 @@ /* what kind of additional features as returned from disco#info are supported? */ extern GList *jabber_features; +extern GList *jabber_identities; +void jabber_stream_features_parse(JabberStream *js, xmlnode *packet); void jabber_process_packet(JabberStream *js, xmlnode **packet); void jabber_send(JabberStream *js, xmlnode *data); void jabber_send_raw(JabberStream *js, const char *data, int len); @@ -283,8 +301,17 @@ */ char *jabber_parse_error(JabberStream *js, xmlnode *packet, PurpleConnectionError *reason); -void jabber_add_feature(const gchar *shortname, const gchar *namespace, JabberFeatureEnabled cb); /* cb may be NULL */ -void jabber_remove_feature(const gchar *shortname); +void jabber_add_feature(const gchar *namespace, JabberFeatureEnabled cb); /* cb may be NULL */ +void jabber_remove_feature(const gchar *namespace); + +/** Adds an identitiy to this jabber library instance. For list of valid values vistit the + * webiste of the XMPP Registrar ( http://www.xmpp.org/registrar/disco-categories.html#client ). + * @param category the category of the identity. + * @param type the type of the identity. + * @param language the language localization of the name. Can be NULL. + * @param name the name of the identity. + */ +void jabber_add_identity(const gchar *category, const gchar *type, const gchar *lang, const gchar *name); /** PRPL functions */ const char *jabber_list_icon(PurpleAccount *a, PurpleBuddy *b); @@ -310,6 +337,8 @@ int jabber_prpl_send_raw(PurpleConnection *gc, const char *buf, int len); GList *jabber_actions(PurplePlugin *plugin, gpointer context); void jabber_register_commands(void); + void jabber_init_plugin(PurplePlugin *plugin); +void jabber_uninit_plugin(void); #endif /* _PURPLE_JABBER_H_ */
--- a/libpurple/protocols/jabber/libxmpp.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/libxmpp.c Tue Feb 17 03:39:22 2009 +0000 @@ -69,7 +69,7 @@ jabber_set_info, /* set_info */ jabber_send_typing, /* send_typing */ jabber_buddy_get_info, /* get_info */ - jabber_presence_send, /* set_status */ + jabber_set_status, /* set_status */ jabber_idle_set, /* set_idle */ NULL, /* change_passwd */ jabber_roster_add_buddy, /* add_buddy */ @@ -148,9 +148,18 @@ purple_signal_unregister(plugin, "jabber-sending-xmlnode"); purple_signal_unregister(plugin, "jabber-sending-text"); - + + /* reverse order of init_plugin */ + jabber_bosh_uninit(); jabber_data_uninit(); - + /* PEP things should be uninit via jabber_pep_uninit, not here */ + jabber_pep_uninit(); + jabber_caps_uninit(); + jabber_iq_uninit(); + + /* Stay on target...stay on target... Almost there... */ + jabber_uninit_plugin(); + return TRUE; } @@ -226,7 +235,13 @@ "auth_plain_in_clear", FALSE); prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); - + + /* XXX FORCE_BOSH: Remove this before re-merging branch */ + option = purple_account_option_bool_new(_("Force BOSH (debugging)"), + "force_bosh", FALSE); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, + option); + option = purple_account_option_int_new(_("Connect port"), "port", 5222); prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); @@ -272,23 +287,24 @@ #endif #endif jabber_register_commands(); - + + /* reverse order of unload_plugin */ jabber_iq_init(); + jabber_caps_init(); + /* PEP things should be init via jabber_pep_init, not here */ jabber_pep_init(); - - jabber_tune_init(); - jabber_caps_init(); - jabber_data_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_bosh_init(); + + #warning implement adding and retrieving own features via IPC API + + jabber_add_feature(AVATARNAMESPACEMETA, jabber_pep_namespace_only_when_pep_enabled_cb); + jabber_add_feature(AVATARNAMESPACEDATA, jabber_pep_namespace_only_when_pep_enabled_cb); + jabber_add_feature("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(XEP_0231_NAMESPACE, jabber_custom_smileys_isenabled); - jabber_pep_register_handler("avatar", AVATARNAMESPACEMETA, jabber_buddy_avatar_update_metadata); + jabber_pep_register_handler(AVATARNAMESPACEMETA, jabber_buddy_avatar_update_metadata); }
--- a/libpurple/protocols/jabber/message.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/message.c Tue Feb 17 03:39:22 2009 +0000 @@ -1236,12 +1236,11 @@ jabber_message_free(jm); } -gboolean jabber_buzz_isenabled(JabberStream *js, const gchar *shortname, const gchar *namespace) { +gboolean jabber_buzz_isenabled(JabberStream *js, const gchar *namespace) { return js->allowBuzz; } -gboolean jabber_custom_smileys_isenabled(JabberStream *js, const gchar *shortname, - const gchar *namespace) +gboolean jabber_custom_smileys_isenabled(JabberStream *js, const gchar *namespace) { const PurpleConnection *gc = js->gc; PurpleAccount *account = purple_connection_get_account(gc);
--- a/libpurple/protocols/jabber/message.h Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/message.h Tue Feb 17 03:39:22 2009 +0000 @@ -78,9 +78,8 @@ unsigned int jabber_send_typing(PurpleConnection *gc, const char *who, PurpleTypingState state); void jabber_message_conv_closed(JabberStream *js, const char *who); -gboolean jabber_buzz_isenabled(JabberStream *js, const gchar *shortname, const gchar *namespace); +gboolean jabber_buzz_isenabled(JabberStream *js, const gchar *namespace); -gboolean jabber_custom_smileys_isenabled(JabberStream *js, const gchar *shortname, - const gchar *namespace); +gboolean jabber_custom_smileys_isenabled(JabberStream *js, const const gchar *namespace); #endif /* _PURPLE_JABBER_MESSAGE_H_ */
--- a/libpurple/protocols/jabber/pep.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/pep.c Tue Feb 17 03:39:22 2009 +0000 @@ -26,6 +26,7 @@ #include <string.h> #include "usermood.h" #include "usernick.h" +#include "usertune.h" static GHashTable *pep_handlers = NULL; @@ -35,19 +36,26 @@ /* register PEP handlers */ jabber_mood_init(); + jabber_tune_init(); jabber_nick_init(); } } +void jabber_pep_uninit(void) { + /* any PEP handlers that need to clean things up go here */ + g_hash_table_destroy(pep_handlers); + pep_handlers = NULL; +} + void jabber_pep_init_actions(GList **m) { /* register the PEP-specific actions */ jabber_mood_init_action(m); jabber_nick_init_action(m); } -void jabber_pep_register_handler(const char *shortname, const char *xmlns, JabberPEPHandler handlerfunc) { +void jabber_pep_register_handler(const char *xmlns, JabberPEPHandler handlerfunc) { gchar *notifyns = g_strdup_printf("%s+notify", xmlns); - jabber_add_feature(shortname, notifyns, NULL); /* receiving PEPs is always supported */ + jabber_add_feature(notifyns, NULL); /* receiving PEPs is always supported */ g_free(notifyns); g_hash_table_replace(pep_handlers, g_strdup(xmlns), handlerfunc); } @@ -85,7 +93,7 @@ jabber_iq_send(iq); } -gboolean jabber_pep_namespace_only_when_pep_enabled_cb(JabberStream *js, const gchar *shortname, const gchar *namespace) { +gboolean jabber_pep_namespace_only_when_pep_enabled_cb(JabberStream *js, const gchar *namespace) { return js->pep; }
--- a/libpurple/protocols/jabber/pep.h Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/pep.h Tue Feb 17 03:39:22 2009 +0000 @@ -27,6 +27,7 @@ #include "buddy.h" void jabber_pep_init(void); +void jabber_pep_uninit(void); void jabber_pep_init_actions(GList **m); @@ -42,11 +43,10 @@ * Registers a callback for PEP events. Also automatically announces this receiving capability via disco#info. * Don't forget to use jabber_add_feature when supporting the sending of PEP events of this type. * - * @parameter shortname A short name for this feature for XEP-0115. It has no semantic meaning, it just has to be unique. - * @parameter xmlns The namespace for this event + * @parameter xmlns The namespace for this event * @parameter handlerfunc The callback to be used when receiving an event with this namespace */ -void jabber_pep_register_handler(const char *shortname, const char *xmlns, JabberPEPHandler handlerfunc); +void jabber_pep_register_handler(const char *xmlns, JabberPEPHandler handlerfunc); /* * Request a specific item from another PEP node. @@ -64,13 +64,12 @@ /* * Default callback that can be used for namespaces which should only be enabled when PEP is supported * - * @parameter js The JabberStream struct for this connection - * @parameter shortname The namespace's shortname (for caps), ignored. + * @parameter js The JabberStream struct for this connection * @parameter namespace The namespace that's queried, ignored. * * @returns TRUE when PEP is enabled, FALSE otherwise */ -gboolean jabber_pep_namespace_only_when_pep_enabled_cb(JabberStream *js, const gchar *shortname, const gchar *namespace); +gboolean jabber_pep_namespace_only_when_pep_enabled_cb(JabberStream *js, const gchar *namespace); void jabber_handle_event(JabberMessage *jm);
--- a/libpurple/protocols/jabber/presence.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/presence.c Tue Feb 17 03:39:22 2009 +0000 @@ -93,11 +93,31 @@ g_free(my_base_jid); } - -void jabber_presence_send(PurpleAccount *account, PurpleStatus *status) +void jabber_set_status(PurpleAccount *account, PurpleStatus *status) { - PurpleConnection *gc = NULL; - JabberStream *js = NULL; + PurpleConnection *gc; + JabberStream *js; + + if (!purple_account_is_connected(account)) + return; + + if (!purple_status_is_active(status)) + return; + + if (purple_status_is_exclusive(status) && !purple_status_is_active(status)) { + /* An exclusive status can't be deactivated. You should just + * activate some other exclusive status. */ + return; + } + + gc = purple_account_get_connection(account); + js = gc->proto_data; + jabber_presence_send(js, FALSE); +} + +void jabber_presence_send(JabberStream *js, gboolean force) +{ + PurpleAccount *account; xmlnode *presence, *x, *photo; char *stripped = NULL; JabberBuddyState state; @@ -106,28 +126,11 @@ int length = -1; gboolean allowBuzz; PurplePresence *p; - PurpleStatus *tune; - - if (purple_account_is_disconnected(account)) - return; - - p = purple_account_get_presence(account); - if (NULL == status) { - status = purple_presence_get_active_status(p); - } + PurpleStatus *status, *tune; - if (purple_status_is_exclusive(status)) { - /* An exclusive status can't be deactivated. You should just - * activate some other exclusive status. */ - if (!purple_status_is_active(status)) - return; - } else { - /* Work with the exclusive status. */ - status = purple_presence_get_active_status(p); - } - - gc = purple_account_get_connection(account); - js = gc->proto_data; + account = purple_connection_get_account(js->gc); + p = purple_account_get_presence(account); + status = purple_presence_get_active_status(p); /* we don't want to send presence before we've gotten our roster */ if(!js->roster_parsed) { @@ -141,16 +144,18 @@ allowBuzz = purple_status_get_attr_boolean(status,"buzz"); /* changing the buzz state has to trigger a re-broadcasting of the presence for caps */ - if (js->googletalk && stripped == NULL && purple_presence_is_status_primitive_active(p, PURPLE_STATUS_TUNE)) { - tune = purple_presence_get_status(p, "tune"); + tune = purple_presence_get_status(p, "tune"); + if (js->googletalk && !stripped && purple_status_is_active(tune)) { stripped = jabber_google_presence_outgoing(tune); } #define CHANGED(a,b) ((!a && b) || (a && a[0] == '\0' && b && b[0] != '\0') || \ (a && !b) || (a && a[0] != '\0' && b && b[0] == '\0') || (a && b && strcmp(a,b))) /* check if there are any differences to the <presence> and send them in that case */ - if (allowBuzz != js->allowBuzz || js->old_state != state || CHANGED(js->old_msg, stripped) || - js->old_priority != priority || CHANGED(js->old_avatarhash, js->avatar_hash)) { + if (force || allowBuzz != js->allowBuzz || js->old_state != state || + CHANGED(js->old_msg, stripped) || js->old_priority != priority || + CHANGED(js->old_avatarhash, js->avatar_hash)) { + /* Need to update allowBuzz before creating the presence (with caps) */ js->allowBuzz = allowBuzz; presence = jabber_presence_create_js(js, state, stripped, priority); @@ -181,8 +186,7 @@ g_free(stripped); /* next, check if there are any changes to the tune values */ - tune = purple_presence_get_status(p, "tune"); - if (tune && purple_status_is_active(tune)) { + if (purple_status_is_active(tune)) { artist = purple_status_get_attr_string(tune, PURPLE_TUNE_ARTIST); title = purple_status_get_attr_string(tune, PURPLE_TUNE_TITLE); source = purple_status_get_attr_string(tune, PURPLE_TUNE_ALBUM); @@ -260,11 +264,16 @@ } /* JEP-0115 */ + /* calculate hash */ + jabber_caps_calculate_own_hash(js); + /* create xml */ c = xmlnode_new_child(presence, "c"); xmlnode_set_namespace(c, "http://jabber.org/protocol/caps"); xmlnode_set_attrib(c, "node", CAPS0115_NODE); - xmlnode_set_attrib(c, "ver", VERSION); - + xmlnode_set_attrib(c, "hash", "sha-1"); + xmlnode_set_attrib(c, "ver", jabber_caps_get_own_hash(js)); + +#if 0 if(js != NULL) { /* add the extensions */ char extlist[1024]; @@ -276,7 +285,7 @@ JabberFeature *feat = (JabberFeature*)feature->data; unsigned featlen; - if(feat->is_enabled != NULL && feat->is_enabled(js, feat->shortname, feat->namespace) == FALSE) + if(feat->is_enabled != NULL && feat->is_enabled(js, feat->namespace) == FALSE) continue; /* skip this feature */ featlen = strlen(feat->shortname); @@ -296,7 +305,7 @@ if(remaining < 1023) xmlnode_set_attrib(c, "ext", extlist); } - +#endif return presence; } @@ -365,39 +374,41 @@ char *from; } JabberPresenceCapabilities; -static void jabber_presence_set_capabilities(JabberCapsClientInfo *info, gpointer user_data) { - JabberPresenceCapabilities *userdata = user_data; - JabberID *jid; +static void +jabber_presence_set_capabilities(JabberCapsClientInfo *info, GList *exts, + JabberPresenceCapabilities *userdata) +{ JabberBuddyResource *jbr; - GList *iter; + char *resource = g_utf8_strrchr(userdata->from, -1, '/'); + resource += 1; - jid = jabber_id_new(userdata->from); - jbr = jabber_buddy_find_resource(userdata->jb, jid->resource); - jabber_id_free(jid); - - if(!jbr) { + jbr = jabber_buddy_find_resource(userdata->jb, resource); + if (!jbr) { g_free(userdata->from); g_free(userdata); + if (exts) { + g_list_foreach(exts, (GFunc)g_free, NULL); + g_list_free(exts); + } return; } - if(jbr->caps) - jabber_caps_free_clientinfo(jbr->caps); - jbr->caps = info; + /* Any old jbr->caps.info is owned by the caps code */ + if (jbr->caps.exts) { + g_list_foreach(jbr->caps.exts, (GFunc)g_free, NULL); + g_list_free(jbr->caps.exts); + } - if (info) { - for(iter = info->features; iter; iter = g_list_next(iter)) { - if(!strcmp((const char*)iter->data, "http://jabber.org/protocol/commands")) { - JabberIq *iq = jabber_iq_new_query(userdata->js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#items"); - xmlnode *query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#items"); - xmlnode_set_attrib(iq->node, "to", userdata->from); - xmlnode_set_attrib(query, "node", "http://jabber.org/protocol/commands"); + jbr->caps.info = info; + jbr->caps.exts = exts; - jabber_iq_set_callback(iq, jabber_adhoc_disco_result_cb, NULL); - jabber_iq_send(iq); - break; - } - } + if (jabber_resource_has_capability(jbr, "http://jabber.org/protocol/commands")) { + JabberIq *iq = jabber_iq_new_query(userdata->js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#items"); + xmlnode *query = xmlnode_get_child_with_namespace(iq->node, "query", "http://jabber.org/protocol/disco#items"); + xmlnode_set_attrib(iq->node, "to", userdata->from); + xmlnode_set_attrib(query, "node", "http://jabber.org/protocol/commands"); + jabber_iq_set_callback(iq, jabber_adhoc_disco_result_cb, NULL); + jabber_iq_send(iq); } g_free(userdata->from); @@ -742,16 +753,22 @@ jbr = jabber_buddy_track_resource(jb, jid->resource, priority, state, status); if(caps) { + /* handle XEP-0115 */ const char *node = xmlnode_get_attrib(caps,"node"); const char *ver = xmlnode_get_attrib(caps,"ver"); + const char *hash = xmlnode_get_attrib(caps,"hash"); const char *ext = xmlnode_get_attrib(caps,"ext"); - - if(node && ver) { + + /* v1.3 uses: node, ver, and optionally ext. + * v1.5 uses: node, ver, and hash. */ + if (node && ver) { JabberPresenceCapabilities *userdata = g_new0(JabberPresenceCapabilities, 1); userdata->js = js; userdata->jb = jb; userdata->from = g_strdup(from); - jabber_caps_get_info(js, from, node, ver, ext, jabber_presence_set_capabilities, userdata); + jabber_caps_get_info(js, from, node, ver, hash, ext, + (jabber_caps_get_info_cb)jabber_presence_set_capabilities, + userdata); } } }
--- a/libpurple/protocols/jabber/presence.h Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/presence.h Tue Feb 17 03:39:22 2009 +0000 @@ -26,7 +26,17 @@ #include "jabber.h" #include "xmlnode.h" -void jabber_presence_send(PurpleAccount *account, PurpleStatus *status); +void jabber_set_status(PurpleAccount *account, PurpleStatus *status); + +/** + * Send a full presence stanza. + * + * @param js A JabberStream object. + * @param force Force sending the presence stanza, irrespective of whether + * the contents seem to have changed. + */ +void jabber_presence_send(JabberStream *js, gboolean force); + xmlnode *jabber_presence_create(JabberBuddyState state, const char *msg, int priority); /* DEPRECATED */ xmlnode *jabber_presence_create_js(JabberStream *js, JabberBuddyState state, const char *msg, int priority); void jabber_presence_parse(JabberStream *js, xmlnode *packet);
--- a/libpurple/protocols/jabber/roster.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/roster.c Tue Feb 17 03:39:22 2009 +0000 @@ -257,11 +257,12 @@ js->currently_parsing_roster_push = FALSE; /* if we're just now parsing the roster for the first time, - * then now would be the time to send our initial presence */ + * then now would be the time to declare ourselves connected and + * send our initial presence */ if(!js->roster_parsed) { js->roster_parsed = TRUE; - - jabber_presence_send(js->gc->account, NULL); + jabber_presence_send(js, TRUE); + jabber_stream_set_state(js, JABBER_STREAM_CONNECTED); } }
--- a/libpurple/protocols/jabber/usermood.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/usermood.c Tue Feb 17 03:39:22 2009 +0000 @@ -141,8 +141,8 @@ } void jabber_mood_init(void) { - jabber_add_feature("mood", "http://jabber.org/protocol/mood", jabber_pep_namespace_only_when_pep_enabled_cb); - jabber_pep_register_handler("moodn", "http://jabber.org/protocol/mood", jabber_mood_cb); + jabber_add_feature("http://jabber.org/protocol/mood", jabber_pep_namespace_only_when_pep_enabled_cb); + jabber_pep_register_handler("http://jabber.org/protocol/mood", jabber_mood_cb); } static void do_mood_set_from_fields(PurpleConnection *gc, PurpleRequestFields *fields) {
--- a/libpurple/protocols/jabber/usernick.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/usernick.c Tue Feb 17 03:39:22 2009 +0000 @@ -92,8 +92,8 @@ } void jabber_nick_init(void) { - jabber_add_feature("nick", "http://jabber.org/protocol/nick", jabber_pep_namespace_only_when_pep_enabled_cb); - jabber_pep_register_handler("nickn", "http://jabber.org/protocol/nick", jabber_nick_cb); + jabber_add_feature("http://jabber.org/protocol/nick", jabber_pep_namespace_only_when_pep_enabled_cb); + jabber_pep_register_handler("http://jabber.org/protocol/nick", jabber_nick_cb); } void jabber_nick_init_action(GList **m) {
--- a/libpurple/protocols/jabber/usertune.c Sun Feb 15 23:11:18 2009 +0000 +++ b/libpurple/protocols/jabber/usertune.c Tue Feb 17 03:39:22 2009 +0000 @@ -109,8 +109,8 @@ } void jabber_tune_init(void) { - jabber_add_feature("tune", "http://jabber.org/protocol/tune", jabber_pep_namespace_only_when_pep_enabled_cb); - jabber_pep_register_handler("tunen", "http://jabber.org/protocol/tune", jabber_tune_cb); + jabber_add_feature("http://jabber.org/protocol/tune", jabber_pep_namespace_only_when_pep_enabled_cb); + jabber_pep_register_handler("http://jabber.org/protocol/tune", jabber_tune_cb); } void jabber_tune_set(PurpleConnection *gc, const PurpleJabberTuneInfo *tuneinfo) {