/src/pidgin/libpurple/protocols/jabber/caps.c
Line | Count | Source |
1 | | /* |
2 | | * purple - Jabber Protocol Plugin |
3 | | * |
4 | | * Purple is the legal property of its developers, whose names are too numerous |
5 | | * to list here. Please refer to the COPYRIGHT file distributed with this |
6 | | * source distribution. |
7 | | * |
8 | | * This program is free software; you can redistribute it and/or modify |
9 | | * it under the terms of the GNU General Public License as published by |
10 | | * the Free Software Foundation; either version 2 of the License, or |
11 | | * (at your option) any later version. |
12 | | * |
13 | | * This program is distributed in the hope that it will be useful, |
14 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | | * GNU General Public License for more details. |
17 | | * |
18 | | * You should have received a copy of the GNU General Public License |
19 | | * along with this program; if not, write to the Free Software |
20 | | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA |
21 | | * |
22 | | */ |
23 | | |
24 | | #include "internal.h" |
25 | | |
26 | | #include "debug.h" |
27 | | #include "caps.h" |
28 | | #include "cipher.h" |
29 | | #include "iq.h" |
30 | | #include "presence.h" |
31 | | #include "util.h" |
32 | | #include "xdata.h" |
33 | | |
34 | 0 | #define JABBER_CAPS_FILENAME "xmpp-caps.xml" |
35 | | |
36 | | typedef struct _JabberDataFormField { |
37 | | gchar *var; |
38 | | GList *values; |
39 | | } JabberDataFormField; |
40 | | |
41 | | static GHashTable *capstable = NULL; /* JabberCapsTuple -> JabberCapsClientInfo */ |
42 | | static GHashTable *nodetable = NULL; /* char *node -> JabberCapsNodeExts */ |
43 | | static guint save_timer = 0; |
44 | | |
45 | | /* Free a GList of allocated char* */ |
46 | | static void |
47 | | free_string_glist(GList *list) |
48 | 0 | { |
49 | 0 | while (list) { |
50 | 0 | g_free(list->data); |
51 | 0 | list = g_list_delete_link(list, list); |
52 | 0 | } |
53 | 0 | } |
54 | | |
55 | | static JabberCapsNodeExts* |
56 | | jabber_caps_node_exts_ref(JabberCapsNodeExts *exts) |
57 | 0 | { |
58 | 0 | g_return_val_if_fail(exts != NULL, NULL); |
59 | | |
60 | 0 | ++exts->ref; |
61 | 0 | return exts; |
62 | 0 | } |
63 | | |
64 | | static void |
65 | | jabber_caps_node_exts_unref(JabberCapsNodeExts *exts) |
66 | 0 | { |
67 | 0 | if (exts == NULL) |
68 | 0 | return; |
69 | | |
70 | 0 | g_return_if_fail(exts->ref != 0); |
71 | | |
72 | 0 | if (--exts->ref != 0) |
73 | 0 | return; |
74 | | |
75 | 0 | g_hash_table_destroy(exts->exts); |
76 | 0 | g_free(exts); |
77 | 0 | } |
78 | | |
79 | 0 | static guint jabber_caps_hash(gconstpointer data) { |
80 | 0 | const JabberCapsTuple *key = data; |
81 | 0 | guint nodehash = g_str_hash(key->node); |
82 | 0 | guint verhash = g_str_hash(key->ver); |
83 | | /* |
84 | | * 'hash' was optional in XEP-0115 v1.4 and g_str_hash crashes on NULL >:O. |
85 | | * Okay, maybe I've played too much Zelda, but that looks like |
86 | | * a Deku Shrub... |
87 | | */ |
88 | 0 | guint hashhash = (key->hash ? g_str_hash(key->hash) : 0); |
89 | 0 | return nodehash ^ verhash ^ hashhash; |
90 | 0 | } |
91 | | |
92 | 0 | static gboolean jabber_caps_compare(gconstpointer v1, gconstpointer v2) { |
93 | 0 | const JabberCapsTuple *name1 = v1; |
94 | 0 | const JabberCapsTuple *name2 = v2; |
95 | |
|
96 | 0 | return purple_strequal(name1->node, name2->node) && |
97 | 0 | purple_strequal(name1->ver, name2->ver) && |
98 | 0 | purple_strequal(name1->hash, name2->hash); |
99 | 0 | } |
100 | | |
101 | | static void |
102 | | jabber_caps_client_info_destroy(JabberCapsClientInfo *info) |
103 | 0 | { |
104 | 0 | if (info == NULL) |
105 | 0 | return; |
106 | | |
107 | 0 | while(info->identities) { |
108 | 0 | JabberIdentity *id = info->identities->data; |
109 | 0 | g_free(id->category); |
110 | 0 | g_free(id->type); |
111 | 0 | g_free(id->name); |
112 | 0 | g_free(id->lang); |
113 | 0 | g_free(id); |
114 | 0 | info->identities = g_list_delete_link(info->identities, info->identities); |
115 | 0 | } |
116 | |
|
117 | 0 | free_string_glist(info->features); |
118 | |
|
119 | 0 | while (info->forms) { |
120 | 0 | xmlnode_free(info->forms->data); |
121 | 0 | info->forms = g_list_delete_link(info->forms, info->forms); |
122 | 0 | } |
123 | |
|
124 | 0 | jabber_caps_node_exts_unref(info->exts); |
125 | |
|
126 | 0 | g_free((char *)info->tuple.node); |
127 | 0 | g_free((char *)info->tuple.ver); |
128 | 0 | g_free((char *)info->tuple.hash); |
129 | |
|
130 | 0 | g_free(info); |
131 | 0 | } |
132 | | |
133 | | /* NOTE: Takes a reference to the exts, unref it if you don't really want to |
134 | | * keep it around. */ |
135 | | static JabberCapsNodeExts* |
136 | | jabber_caps_find_exts_by_node(const char *node) |
137 | 0 | { |
138 | 0 | JabberCapsNodeExts *exts; |
139 | 0 | if (NULL == (exts = g_hash_table_lookup(nodetable, node))) { |
140 | 0 | exts = g_new0(JabberCapsNodeExts, 1); |
141 | 0 | exts->exts = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, |
142 | 0 | (GDestroyNotify)free_string_glist); |
143 | 0 | g_hash_table_insert(nodetable, g_strdup(node), jabber_caps_node_exts_ref(exts)); |
144 | 0 | } |
145 | |
|
146 | 0 | return jabber_caps_node_exts_ref(exts); |
147 | 0 | } |
148 | | |
149 | | static void |
150 | | exts_to_xmlnode(gconstpointer key, gconstpointer value, gpointer user_data) |
151 | 0 | { |
152 | 0 | const char *identifier = key; |
153 | 0 | const GList *features = value, *node; |
154 | 0 | xmlnode *client = user_data, *ext, *feature; |
155 | |
|
156 | 0 | ext = xmlnode_new_child(client, "ext"); |
157 | 0 | xmlnode_set_attrib(ext, "identifier", identifier); |
158 | |
|
159 | 0 | for (node = features; node; node = node->next) { |
160 | 0 | feature = xmlnode_new_child(ext, "feature"); |
161 | 0 | xmlnode_set_attrib(feature, "var", (const gchar *)node->data); |
162 | 0 | } |
163 | 0 | } |
164 | | |
165 | 0 | static void jabber_caps_store_client(gpointer key, gpointer value, gpointer user_data) { |
166 | 0 | const JabberCapsTuple *tuple = key; |
167 | 0 | const JabberCapsClientInfo *props = value; |
168 | 0 | xmlnode *root = user_data; |
169 | 0 | xmlnode *client = xmlnode_new_child(root, "client"); |
170 | 0 | GList *iter; |
171 | |
|
172 | 0 | xmlnode_set_attrib(client, "node", tuple->node); |
173 | 0 | xmlnode_set_attrib(client, "ver", tuple->ver); |
174 | 0 | if (tuple->hash) |
175 | 0 | xmlnode_set_attrib(client, "hash", tuple->hash); |
176 | 0 | for(iter = props->identities; iter; iter = g_list_next(iter)) { |
177 | 0 | JabberIdentity *id = iter->data; |
178 | 0 | xmlnode *identity = xmlnode_new_child(client, "identity"); |
179 | 0 | xmlnode_set_attrib(identity, "category", id->category); |
180 | 0 | xmlnode_set_attrib(identity, "type", id->type); |
181 | 0 | if (id->name) |
182 | 0 | xmlnode_set_attrib(identity, "name", id->name); |
183 | 0 | if (id->lang) |
184 | 0 | xmlnode_set_attrib(identity, "lang", id->lang); |
185 | 0 | } |
186 | |
|
187 | 0 | for(iter = props->features; iter; iter = g_list_next(iter)) { |
188 | 0 | const char *feat = iter->data; |
189 | 0 | xmlnode *feature = xmlnode_new_child(client, "feature"); |
190 | 0 | xmlnode_set_attrib(feature, "var", feat); |
191 | 0 | } |
192 | |
|
193 | 0 | for(iter = props->forms; iter; iter = g_list_next(iter)) { |
194 | | /* FIXME: See #7814 */ |
195 | 0 | xmlnode *xdata = iter->data; |
196 | 0 | xmlnode_insert_child(client, xmlnode_copy(xdata)); |
197 | 0 | } |
198 | | |
199 | | /* TODO: Ideally, only save this once-per-node... */ |
200 | 0 | if (props->exts) |
201 | 0 | g_hash_table_foreach(props->exts->exts, (GHFunc)exts_to_xmlnode, client); |
202 | 0 | } |
203 | | |
204 | | static gboolean |
205 | | do_jabber_caps_store(gpointer data) |
206 | 0 | { |
207 | 0 | char *str; |
208 | 0 | int length = 0; |
209 | 0 | xmlnode *root = xmlnode_new("capabilities"); |
210 | 0 | g_hash_table_foreach(capstable, jabber_caps_store_client, root); |
211 | 0 | str = xmlnode_to_formatted_str(root, &length); |
212 | 0 | xmlnode_free(root); |
213 | 0 | purple_util_write_data_to_file(JABBER_CAPS_FILENAME, str, length); |
214 | 0 | g_free(str); |
215 | |
|
216 | 0 | save_timer = 0; |
217 | 0 | return FALSE; |
218 | 0 | } |
219 | | |
220 | | static void |
221 | | schedule_caps_save(void) |
222 | 0 | { |
223 | 0 | if (save_timer == 0) |
224 | 0 | save_timer = purple_timeout_add_seconds(5, do_jabber_caps_store, NULL); |
225 | 0 | } |
226 | | |
227 | | static void |
228 | | jabber_caps_load(void) |
229 | 0 | { |
230 | 0 | xmlnode *capsdata = purple_util_read_xml_from_file(JABBER_CAPS_FILENAME, "XMPP capabilities cache"); |
231 | 0 | xmlnode *client; |
232 | |
|
233 | 0 | if(!capsdata) |
234 | 0 | return; |
235 | | |
236 | 0 | if (!purple_strequal(capsdata->name, "capabilities")) { |
237 | 0 | xmlnode_free(capsdata); |
238 | 0 | return; |
239 | 0 | } |
240 | | |
241 | 0 | for (client = capsdata->child; client; client = client->next) { |
242 | 0 | if (client->type != XMLNODE_TYPE_TAG) |
243 | 0 | continue; |
244 | 0 | if (purple_strequal(client->name, "client")) { |
245 | 0 | JabberCapsClientInfo *value = g_new0(JabberCapsClientInfo, 1); |
246 | 0 | JabberCapsTuple *key = (JabberCapsTuple*)&value->tuple; |
247 | 0 | xmlnode *child; |
248 | 0 | JabberCapsNodeExts *exts = NULL; |
249 | 0 | key->node = g_strdup(xmlnode_get_attrib(client,"node")); |
250 | 0 | key->ver = g_strdup(xmlnode_get_attrib(client,"ver")); |
251 | 0 | key->hash = g_strdup(xmlnode_get_attrib(client,"hash")); |
252 | | |
253 | | /* v1.3 capabilities */ |
254 | 0 | if (key->hash == NULL) |
255 | 0 | exts = jabber_caps_find_exts_by_node(key->node); |
256 | |
|
257 | 0 | for (child = client->child; child; child = child->next) { |
258 | 0 | if (child->type != XMLNODE_TYPE_TAG) |
259 | 0 | continue; |
260 | 0 | if (purple_strequal(child->name, "feature")) { |
261 | 0 | const char *var = xmlnode_get_attrib(child, "var"); |
262 | 0 | if(!var) |
263 | 0 | continue; |
264 | 0 | value->features = g_list_append(value->features,g_strdup(var)); |
265 | 0 | } else if (purple_strequal(child->name, "identity")) { |
266 | 0 | const char *category = xmlnode_get_attrib(child, "category"); |
267 | 0 | const char *type = xmlnode_get_attrib(child, "type"); |
268 | 0 | const char *name = xmlnode_get_attrib(child, "name"); |
269 | 0 | const char *lang = xmlnode_get_attrib(child, "lang"); |
270 | 0 | JabberIdentity *id; |
271 | |
|
272 | 0 | if (!category || !type) |
273 | 0 | continue; |
274 | | |
275 | 0 | id = g_new0(JabberIdentity, 1); |
276 | 0 | id->category = g_strdup(category); |
277 | 0 | id->type = g_strdup(type); |
278 | 0 | id->name = g_strdup(name); |
279 | 0 | id->lang = g_strdup(lang); |
280 | |
|
281 | 0 | value->identities = g_list_append(value->identities,id); |
282 | 0 | } else if (purple_strequal(child->name, "x")) { |
283 | | /* TODO: See #7814 -- this might cause problems if anyone |
284 | | * ever actually specifies forms. In fact, for this to |
285 | | * work properly, that bug needs to be fixed in |
286 | | * xmlnode_from_str, not the output version... */ |
287 | 0 | value->forms = g_list_append(value->forms, xmlnode_copy(child)); |
288 | 0 | } else if (purple_strequal(child->name, "ext")) { |
289 | 0 | if (key->hash != NULL) |
290 | 0 | purple_debug_warning("jabber", "Ignoring exts when reading new-style caps\n"); |
291 | 0 | else { |
292 | | /* TODO: Do we care about reading in the identities listed here? */ |
293 | 0 | const char *identifier = xmlnode_get_attrib(child, "identifier"); |
294 | 0 | xmlnode *node; |
295 | 0 | GList *features = NULL; |
296 | |
|
297 | 0 | if (!identifier) |
298 | 0 | continue; |
299 | | |
300 | 0 | for (node = child->child; node; node = node->next) { |
301 | 0 | if (node->type != XMLNODE_TYPE_TAG) |
302 | 0 | continue; |
303 | 0 | if (purple_strequal(node->name, "feature")) { |
304 | 0 | const char *var = xmlnode_get_attrib(node, "var"); |
305 | 0 | if (!var) |
306 | 0 | continue; |
307 | 0 | features = g_list_prepend(features, g_strdup(var)); |
308 | 0 | } |
309 | 0 | } |
310 | |
|
311 | 0 | if (features) { |
312 | 0 | g_hash_table_insert(exts->exts, g_strdup(identifier), |
313 | 0 | features); |
314 | 0 | } else |
315 | 0 | purple_debug_warning("jabber", "Caps ext %s had no features.\n", |
316 | 0 | identifier); |
317 | 0 | } |
318 | 0 | } |
319 | 0 | } |
320 | |
|
321 | 0 | value->exts = exts; |
322 | 0 | g_hash_table_replace(capstable, key, value); |
323 | |
|
324 | 0 | } |
325 | 0 | } |
326 | 0 | xmlnode_free(capsdata); |
327 | 0 | } |
328 | | |
329 | | void jabber_caps_init(void) |
330 | 0 | { |
331 | 0 | nodetable = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)jabber_caps_node_exts_unref); |
332 | 0 | capstable = g_hash_table_new_full(jabber_caps_hash, jabber_caps_compare, NULL, (GDestroyNotify)jabber_caps_client_info_destroy); |
333 | 0 | jabber_caps_load(); |
334 | 0 | } |
335 | | |
336 | | void jabber_caps_uninit(void) |
337 | 0 | { |
338 | 0 | if (save_timer != 0) { |
339 | 0 | purple_timeout_remove(save_timer); |
340 | 0 | save_timer = 0; |
341 | 0 | do_jabber_caps_store(NULL); |
342 | 0 | } |
343 | 0 | g_hash_table_destroy(capstable); |
344 | 0 | g_hash_table_destroy(nodetable); |
345 | 0 | capstable = nodetable = NULL; |
346 | 0 | } |
347 | | |
348 | | gboolean jabber_caps_exts_known(const JabberCapsClientInfo *info, |
349 | | char **exts) |
350 | 0 | { |
351 | 0 | int i; |
352 | 0 | g_return_val_if_fail(info != NULL, FALSE); |
353 | | |
354 | 0 | if (!exts) |
355 | 0 | return TRUE; |
356 | | |
357 | 0 | for (i = 0; exts[i]; ++i) { |
358 | | /* Hack since we advertise the ext along with v1.5 caps but don't |
359 | | * store any exts */ |
360 | 0 | if (purple_strequal(exts[i], "voice-v1") && !info->exts) |
361 | 0 | continue; |
362 | 0 | if (!info->exts || |
363 | 0 | !g_hash_table_lookup(info->exts->exts, exts[i])) |
364 | 0 | return FALSE; |
365 | 0 | } |
366 | | |
367 | 0 | return TRUE; |
368 | 0 | } |
369 | | |
370 | | typedef struct _jabber_caps_cbplususerdata { |
371 | | guint ref; |
372 | | |
373 | | jabber_caps_get_info_cb cb; |
374 | | gpointer cb_data; |
375 | | |
376 | | char *who; |
377 | | char *node; |
378 | | char *ver; |
379 | | char *hash; |
380 | | |
381 | | JabberCapsClientInfo *info; |
382 | | |
383 | | GList *exts; |
384 | | guint extOutstanding; |
385 | | JabberCapsNodeExts *node_exts; |
386 | | } jabber_caps_cbplususerdata; |
387 | | |
388 | | static jabber_caps_cbplususerdata* |
389 | | cbplususerdata_ref(jabber_caps_cbplususerdata *data) |
390 | 0 | { |
391 | 0 | g_return_val_if_fail(data != NULL, NULL); |
392 | | |
393 | 0 | ++data->ref; |
394 | 0 | return data; |
395 | 0 | } |
396 | | |
397 | | static void |
398 | | cbplususerdata_unref(jabber_caps_cbplususerdata *data) |
399 | 0 | { |
400 | 0 | if (data == NULL) |
401 | 0 | return; |
402 | | |
403 | 0 | g_return_if_fail(data->ref != 0); |
404 | | |
405 | 0 | if (--data->ref > 0) |
406 | 0 | return; |
407 | | |
408 | 0 | g_free(data->who); |
409 | 0 | g_free(data->node); |
410 | 0 | g_free(data->ver); |
411 | 0 | g_free(data->hash); |
412 | | |
413 | | /* If we have info here, it's already in the capstable, so don't free it */ |
414 | 0 | if (data->exts) |
415 | 0 | free_string_glist(data->exts); |
416 | 0 | if (data->node_exts) |
417 | 0 | jabber_caps_node_exts_unref(data->node_exts); |
418 | 0 | g_free(data); |
419 | 0 | } |
420 | | |
421 | | static void |
422 | | jabber_caps_get_info_complete(jabber_caps_cbplususerdata *userdata) |
423 | 0 | { |
424 | 0 | if (userdata->cb) { |
425 | 0 | userdata->cb(userdata->info, userdata->exts, userdata->cb_data); |
426 | 0 | userdata->info = NULL; |
427 | 0 | userdata->exts = NULL; |
428 | 0 | } |
429 | |
|
430 | 0 | if (userdata->ref != 1) |
431 | 0 | purple_debug_warning("jabber", "Lost a reference to caps cbdata: %d\n", |
432 | 0 | userdata->ref); |
433 | 0 | } |
434 | | |
435 | | static void |
436 | | jabber_caps_client_iqcb(JabberStream *js, const char *from, JabberIqType type, |
437 | | const char *id, xmlnode *packet, gpointer data) |
438 | 0 | { |
439 | 0 | xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", |
440 | 0 | NS_DISCO_INFO); |
441 | 0 | jabber_caps_cbplususerdata *userdata = data; |
442 | 0 | JabberCapsClientInfo *info = NULL, *value; |
443 | 0 | JabberCapsTuple key; |
444 | |
|
445 | 0 | if (!query || type == JABBER_IQ_ERROR) { |
446 | | /* Any outstanding exts will be dealt with via ref-counting */ |
447 | 0 | userdata->cb(NULL, NULL, userdata->cb_data); |
448 | 0 | cbplususerdata_unref(userdata); |
449 | 0 | return; |
450 | 0 | } |
451 | | |
452 | | /* check hash */ |
453 | 0 | info = jabber_caps_parse_client_info(query); |
454 | | |
455 | | /* Only validate if these are v1.5 capabilities */ |
456 | 0 | if (userdata->hash) { |
457 | 0 | gchar *hash = NULL; |
458 | | /* |
459 | | * TODO: If you add *any* hash here, make sure the checksum buffer |
460 | | * size in jabber_caps_calculate_hash is large enough. The cipher API |
461 | | * doesn't seem to offer a "Get the hash size" function(?). |
462 | | */ |
463 | 0 | if (purple_strequal(userdata->hash, "sha-1")) { |
464 | 0 | hash = jabber_caps_calculate_hash(info, "sha1"); |
465 | 0 | } else if (purple_strequal(userdata->hash, "md5")) { |
466 | 0 | hash = jabber_caps_calculate_hash(info, "md5"); |
467 | 0 | } |
468 | |
|
469 | 0 | if (!hash || !purple_strequal(hash, userdata->ver)) { |
470 | 0 | purple_debug_warning("jabber", "Could not validate caps info from " |
471 | 0 | "%s. Expected %s, got %s\n", |
472 | 0 | xmlnode_get_attrib(packet, "from"), |
473 | 0 | userdata->ver, hash ? hash : "(null)"); |
474 | |
|
475 | 0 | userdata->cb(NULL, NULL, userdata->cb_data); |
476 | 0 | jabber_caps_client_info_destroy(info); |
477 | 0 | cbplususerdata_unref(userdata); |
478 | 0 | g_free(hash); |
479 | 0 | return; |
480 | 0 | } |
481 | | |
482 | 0 | g_free(hash); |
483 | 0 | } |
484 | | |
485 | 0 | if (!userdata->hash && userdata->node_exts) { |
486 | | /* If the ClientInfo doesn't have information about the exts, give them |
487 | | * ours (along with our ref) */ |
488 | 0 | info->exts = userdata->node_exts; |
489 | 0 | userdata->node_exts = NULL; |
490 | 0 | } |
491 | |
|
492 | 0 | key.node = userdata->node; |
493 | 0 | key.ver = userdata->ver; |
494 | 0 | key.hash = userdata->hash; |
495 | | |
496 | | /* Use the copy of this data already in the table if it exists or insert |
497 | | * a new one if we need to */ |
498 | 0 | if ((value = g_hash_table_lookup(capstable, &key))) { |
499 | 0 | jabber_caps_client_info_destroy(info); |
500 | 0 | info = value; |
501 | 0 | } else { |
502 | 0 | JabberCapsTuple *n_key = (JabberCapsTuple *)&info->tuple; |
503 | 0 | n_key->node = userdata->node; |
504 | 0 | n_key->ver = userdata->ver; |
505 | 0 | n_key->hash = userdata->hash; |
506 | 0 | userdata->node = userdata->ver = userdata->hash = NULL; |
507 | | |
508 | | /* The capstable gets a reference */ |
509 | 0 | g_hash_table_insert(capstable, n_key, info); |
510 | 0 | schedule_caps_save(); |
511 | 0 | } |
512 | |
|
513 | 0 | userdata->info = info; |
514 | |
|
515 | 0 | if (userdata->extOutstanding == 0) |
516 | 0 | jabber_caps_get_info_complete(userdata); |
517 | |
|
518 | 0 | cbplususerdata_unref(userdata); |
519 | 0 | } |
520 | | |
521 | | typedef struct { |
522 | | const char *name; |
523 | | jabber_caps_cbplususerdata *data; |
524 | | } ext_iq_data; |
525 | | |
526 | | static void |
527 | | jabber_caps_ext_iqcb(JabberStream *js, const char *from, JabberIqType type, |
528 | | const char *id, xmlnode *packet, gpointer data) |
529 | 0 | { |
530 | 0 | xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", |
531 | 0 | NS_DISCO_INFO); |
532 | 0 | xmlnode *child; |
533 | 0 | ext_iq_data *userdata = data; |
534 | 0 | GList *features = NULL; |
535 | 0 | JabberCapsNodeExts *node_exts; |
536 | |
|
537 | 0 | if (!query || type == JABBER_IQ_ERROR) { |
538 | 0 | cbplususerdata_unref(userdata->data); |
539 | 0 | g_free(userdata); |
540 | 0 | return; |
541 | 0 | } |
542 | | |
543 | 0 | node_exts = (userdata->data->info ? userdata->data->info->exts : |
544 | 0 | userdata->data->node_exts); |
545 | | |
546 | | /* TODO: I don't see how this can actually happen, but it crashed khc. */ |
547 | 0 | if (!node_exts) { |
548 | 0 | purple_debug_error("jabber", "Couldn't find JabberCapsNodeExts. If you " |
549 | 0 | "see this, please tell darkrain42 and save your debug log.\n" |
550 | 0 | "JabberCapsClientInfo = %p\n", userdata->data->info); |
551 | | |
552 | | |
553 | | /* Try once more to find the exts and then fail */ |
554 | 0 | node_exts = jabber_caps_find_exts_by_node(userdata->data->node); |
555 | 0 | if (node_exts) { |
556 | 0 | purple_debug_info("jabber", "Found the exts on the second try.\n"); |
557 | 0 | if (userdata->data->info) |
558 | 0 | userdata->data->info->exts = node_exts; |
559 | 0 | else |
560 | 0 | userdata->data->node_exts = node_exts; |
561 | 0 | } else { |
562 | 0 | cbplususerdata_unref(userdata->data); |
563 | 0 | g_free(userdata); |
564 | 0 | g_return_if_reached(); |
565 | 0 | } |
566 | 0 | } |
567 | | |
568 | | /* So, we decrement this after checking for an error, which means that |
569 | | * if there *is* an error, we'll never call the callback passed to |
570 | | * jabber_caps_get_info. We will still free all of our data, though. |
571 | | */ |
572 | 0 | --userdata->data->extOutstanding; |
573 | |
|
574 | 0 | for (child = xmlnode_get_child(query, "feature"); child; |
575 | 0 | child = xmlnode_get_next_twin(child)) { |
576 | 0 | const char *var = xmlnode_get_attrib(child, "var"); |
577 | 0 | if (var) |
578 | 0 | features = g_list_prepend(features, g_strdup(var)); |
579 | 0 | } |
580 | |
|
581 | 0 | g_hash_table_insert(node_exts->exts, g_strdup(userdata->name), features); |
582 | 0 | schedule_caps_save(); |
583 | | |
584 | | /* Are we done? */ |
585 | 0 | if (userdata->data->info && userdata->data->extOutstanding == 0) |
586 | 0 | jabber_caps_get_info_complete(userdata->data); |
587 | |
|
588 | 0 | cbplususerdata_unref(userdata->data); |
589 | 0 | g_free(userdata); |
590 | 0 | } |
591 | | |
592 | | void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, |
593 | | const char *ver, const char *hash, char **exts, |
594 | | jabber_caps_get_info_cb cb, gpointer user_data) |
595 | 0 | { |
596 | 0 | JabberCapsClientInfo *info; |
597 | 0 | JabberCapsTuple key; |
598 | 0 | jabber_caps_cbplususerdata *userdata; |
599 | |
|
600 | 0 | if (exts && hash) { |
601 | 0 | purple_debug_misc("jabber", "Ignoring exts in new-style caps from %s\n", |
602 | 0 | who); |
603 | 0 | g_strfreev(exts); |
604 | 0 | exts = NULL; |
605 | 0 | } |
606 | | |
607 | | /* Using this in a read-only fashion, so the cast is OK */ |
608 | 0 | key.node = (char *)node; |
609 | 0 | key.ver = (char *)ver; |
610 | 0 | key.hash = (char *)hash; |
611 | |
|
612 | 0 | info = g_hash_table_lookup(capstable, &key); |
613 | 0 | if (info && hash) { |
614 | | /* v1.5 - We already have all the information we care about */ |
615 | 0 | if (cb) |
616 | 0 | cb(info, NULL, user_data); |
617 | 0 | return; |
618 | 0 | } |
619 | | |
620 | 0 | userdata = g_new0(jabber_caps_cbplususerdata, 1); |
621 | | /* We start out with 0 references. Every query takes one */ |
622 | 0 | userdata->cb = cb; |
623 | 0 | userdata->cb_data = user_data; |
624 | 0 | userdata->who = g_strdup(who); |
625 | 0 | userdata->node = g_strdup(node); |
626 | 0 | userdata->ver = g_strdup(ver); |
627 | 0 | userdata->hash = g_strdup(hash); |
628 | |
|
629 | 0 | if (info) { |
630 | 0 | userdata->info = info; |
631 | 0 | } else { |
632 | | /* If we don't have the basic information about the client, we need |
633 | | * to fetch it. */ |
634 | 0 | JabberIq *iq; |
635 | 0 | xmlnode *query; |
636 | 0 | char *nodever; |
637 | |
|
638 | 0 | iq = jabber_iq_new_query(js, JABBER_IQ_GET, NS_DISCO_INFO); |
639 | 0 | query = xmlnode_get_child_with_namespace(iq->node, "query", |
640 | 0 | NS_DISCO_INFO); |
641 | 0 | nodever = g_strdup_printf("%s#%s", node, ver); |
642 | 0 | xmlnode_set_attrib(query, "node", nodever); |
643 | 0 | g_free(nodever); |
644 | 0 | xmlnode_set_attrib(iq->node, "to", who); |
645 | |
|
646 | 0 | cbplususerdata_ref(userdata); |
647 | |
|
648 | 0 | jabber_iq_set_callback(iq, jabber_caps_client_iqcb, userdata); |
649 | 0 | jabber_iq_send(iq); |
650 | 0 | } |
651 | | |
652 | | /* Are there any exts that we don't recognize? */ |
653 | 0 | if (exts) { |
654 | 0 | JabberCapsNodeExts *node_exts; |
655 | 0 | int i; |
656 | |
|
657 | 0 | if (info) { |
658 | 0 | if (info->exts) |
659 | 0 | node_exts = info->exts; |
660 | 0 | else |
661 | 0 | node_exts = info->exts = jabber_caps_find_exts_by_node(node); |
662 | 0 | } else |
663 | | /* We'll put it in later once we have the client info */ |
664 | 0 | node_exts = userdata->node_exts = jabber_caps_find_exts_by_node(node); |
665 | |
|
666 | 0 | for (i = 0; exts[i]; ++i) { |
667 | 0 | userdata->exts = g_list_prepend(userdata->exts, exts[i]); |
668 | | /* Look it up if we don't already know what it means */ |
669 | 0 | if (!g_hash_table_lookup(node_exts->exts, exts[i])) { |
670 | 0 | JabberIq *iq; |
671 | 0 | xmlnode *query; |
672 | 0 | char *nodeext; |
673 | 0 | ext_iq_data *cbdata = g_new(ext_iq_data, 1); |
674 | |
|
675 | 0 | cbdata->name = exts[i]; |
676 | 0 | cbdata->data = cbplususerdata_ref(userdata); |
677 | |
|
678 | 0 | iq = jabber_iq_new_query(js, JABBER_IQ_GET, NS_DISCO_INFO); |
679 | 0 | query = xmlnode_get_child_with_namespace(iq->node, "query", |
680 | 0 | NS_DISCO_INFO); |
681 | 0 | nodeext = g_strdup_printf("%s#%s", node, exts[i]); |
682 | 0 | xmlnode_set_attrib(query, "node", nodeext); |
683 | 0 | g_free(nodeext); |
684 | 0 | xmlnode_set_attrib(iq->node, "to", who); |
685 | |
|
686 | 0 | jabber_iq_set_callback(iq, jabber_caps_ext_iqcb, cbdata); |
687 | 0 | jabber_iq_send(iq); |
688 | |
|
689 | 0 | ++userdata->extOutstanding; |
690 | 0 | } |
691 | 0 | exts[i] = NULL; |
692 | 0 | } |
693 | | /* All the strings are now part of the GList, so don't need |
694 | | * g_strfreev. */ |
695 | 0 | g_free(exts); |
696 | 0 | } |
697 | |
|
698 | 0 | if (userdata->info && userdata->extOutstanding == 0) { |
699 | | /* Start with 1 ref so the below functions are happy */ |
700 | 0 | userdata->ref = 1; |
701 | | |
702 | | /* We have everything we need right now */ |
703 | 0 | jabber_caps_get_info_complete(userdata); |
704 | 0 | cbplususerdata_unref(userdata); |
705 | 0 | } |
706 | 0 | } |
707 | | |
708 | | static gint |
709 | | jabber_xdata_compare(gconstpointer a, gconstpointer b) |
710 | 0 | { |
711 | 0 | const xmlnode *aformtypefield = a; |
712 | 0 | const xmlnode *bformtypefield = b; |
713 | 0 | char *aformtype; |
714 | 0 | char *bformtype; |
715 | 0 | int result; |
716 | |
|
717 | 0 | aformtype = jabber_x_data_get_formtype(aformtypefield); |
718 | 0 | bformtype = jabber_x_data_get_formtype(bformtypefield); |
719 | |
|
720 | 0 | result = strcmp(aformtype, bformtype); |
721 | 0 | g_free(aformtype); |
722 | 0 | g_free(bformtype); |
723 | 0 | return result; |
724 | 0 | } |
725 | | |
726 | | JabberCapsClientInfo *jabber_caps_parse_client_info(xmlnode *query) |
727 | 0 | { |
728 | 0 | xmlnode *child; |
729 | 0 | JabberCapsClientInfo *info; |
730 | |
|
731 | 0 | if (!query || !purple_strequal(query->name, "query") || |
732 | 0 | !purple_strequal(query->xmlns, NS_DISCO_INFO)) |
733 | 0 | return NULL; |
734 | | |
735 | 0 | info = g_new0(JabberCapsClientInfo, 1); |
736 | |
|
737 | 0 | for(child = query->child; child; child = child->next) { |
738 | 0 | if (child->type != XMLNODE_TYPE_TAG) |
739 | 0 | continue; |
740 | 0 | if (purple_strequal(child->name, "identity")) { |
741 | | /* parse identity */ |
742 | 0 | const char *category = xmlnode_get_attrib(child, "category"); |
743 | 0 | const char *type = xmlnode_get_attrib(child, "type"); |
744 | 0 | const char *name = xmlnode_get_attrib(child, "name"); |
745 | 0 | const char *lang = xmlnode_get_attrib(child, "lang"); |
746 | 0 | JabberIdentity *id; |
747 | |
|
748 | 0 | if (!category || !type) |
749 | 0 | continue; |
750 | | |
751 | 0 | id = g_new0(JabberIdentity, 1); |
752 | 0 | id->category = g_strdup(category); |
753 | 0 | id->type = g_strdup(type); |
754 | 0 | id->name = g_strdup(name); |
755 | 0 | id->lang = g_strdup(lang); |
756 | |
|
757 | 0 | info->identities = g_list_append(info->identities, id); |
758 | 0 | } else if (purple_strequal(child->name, "feature")) { |
759 | | /* parse feature */ |
760 | 0 | const char *var = xmlnode_get_attrib(child, "var"); |
761 | 0 | if (var) |
762 | 0 | info->features = g_list_prepend(info->features, g_strdup(var)); |
763 | 0 | } else if (purple_strequal(child->name, "x")) { |
764 | 0 | if (purple_strequal(child->xmlns, "jabber:x:data")) { |
765 | | /* x-data form */ |
766 | 0 | xmlnode *dataform = xmlnode_copy(child); |
767 | 0 | info->forms = g_list_append(info->forms, dataform); |
768 | 0 | } |
769 | 0 | } |
770 | 0 | } |
771 | 0 | return info; |
772 | 0 | } |
773 | | |
774 | | static gint jabber_caps_xdata_field_compare(gconstpointer a, gconstpointer b) |
775 | 0 | { |
776 | 0 | const JabberDataFormField *ac = a; |
777 | 0 | const JabberDataFormField *bc = b; |
778 | |
|
779 | 0 | return strcmp(ac->var, bc->var); |
780 | 0 | } |
781 | | |
782 | | static GList* jabber_caps_xdata_get_fields(const xmlnode *x) |
783 | 0 | { |
784 | 0 | GList *fields = NULL; |
785 | 0 | xmlnode *field; |
786 | |
|
787 | 0 | if (!x) |
788 | 0 | return NULL; |
789 | | |
790 | 0 | for (field = xmlnode_get_child(x, "field"); field; field = xmlnode_get_next_twin(field)) { |
791 | 0 | xmlnode *value; |
792 | 0 | JabberDataFormField *xdatafield = g_new0(JabberDataFormField, 1); |
793 | 0 | xdatafield->var = g_strdup(xmlnode_get_attrib(field, "var")); |
794 | |
|
795 | 0 | for (value = xmlnode_get_child(field, "value"); value; value = xmlnode_get_next_twin(value)) { |
796 | 0 | gchar *val = xmlnode_get_data(value); |
797 | 0 | xdatafield->values = g_list_append(xdatafield->values, val); |
798 | 0 | } |
799 | |
|
800 | 0 | xdatafield->values = g_list_sort(xdatafield->values, (GCompareFunc)strcmp); |
801 | 0 | fields = g_list_append(fields, xdatafield); |
802 | 0 | } |
803 | |
|
804 | 0 | fields = g_list_sort(fields, jabber_caps_xdata_field_compare); |
805 | 0 | return fields; |
806 | 0 | } |
807 | | |
808 | | static void |
809 | | append_escaped_string(PurpleCipherContext *context, const gchar *str) |
810 | 0 | { |
811 | 0 | if (str && *str) { |
812 | 0 | char *tmp = g_markup_escape_text(str, -1); |
813 | 0 | purple_cipher_context_append(context, (const guchar *)tmp, strlen(tmp)); |
814 | 0 | g_free(tmp); |
815 | 0 | } |
816 | |
|
817 | 0 | purple_cipher_context_append(context, (const guchar *)"<", 1); |
818 | 0 | } |
819 | | |
820 | | gchar *jabber_caps_calculate_hash(JabberCapsClientInfo *info, const char *hash) |
821 | 0 | { |
822 | 0 | GList *node; |
823 | 0 | PurpleCipherContext *context; |
824 | 0 | guint8 checksum[20]; |
825 | 0 | gsize checksum_size = 20; |
826 | 0 | gboolean success; |
827 | |
|
828 | 0 | if (!info || !(context = purple_cipher_context_new_by_name(hash, NULL))) |
829 | 0 | return NULL; |
830 | | |
831 | | /* sort identities, features and x-data forms */ |
832 | 0 | info->identities = g_list_sort(info->identities, jabber_identity_compare); |
833 | 0 | info->features = g_list_sort(info->features, (GCompareFunc)strcmp); |
834 | 0 | info->forms = g_list_sort(info->forms, jabber_xdata_compare); |
835 | | |
836 | | /* Add identities to the hash data */ |
837 | 0 | for (node = info->identities; node; node = node->next) { |
838 | 0 | JabberIdentity *id = (JabberIdentity*)node->data; |
839 | 0 | char *category = g_markup_escape_text(id->category, -1); |
840 | 0 | char *type = g_markup_escape_text(id->type, -1); |
841 | 0 | char *lang = NULL; |
842 | 0 | char *name = NULL; |
843 | 0 | char *tmp; |
844 | |
|
845 | 0 | if (id->lang) |
846 | 0 | lang = g_markup_escape_text(id->lang, -1); |
847 | 0 | if (id->name) |
848 | 0 | name = g_markup_escape_text(id->name, -1); |
849 | |
|
850 | 0 | tmp = g_strconcat(category, "/", type, "/", lang ? lang : "", |
851 | 0 | "/", name ? name : "", "<", NULL); |
852 | |
|
853 | 0 | purple_cipher_context_append(context, (const guchar *)tmp, strlen(tmp)); |
854 | |
|
855 | 0 | g_free(tmp); |
856 | 0 | g_free(category); |
857 | 0 | g_free(type); |
858 | 0 | g_free(lang); |
859 | 0 | g_free(name); |
860 | 0 | } |
861 | | |
862 | | /* concat features to the verification string */ |
863 | 0 | for (node = info->features; node; node = node->next) { |
864 | 0 | append_escaped_string(context, node->data); |
865 | 0 | } |
866 | | |
867 | | /* concat x-data forms to the verification string */ |
868 | 0 | for(node = info->forms; node; node = node->next) { |
869 | 0 | xmlnode *data = (xmlnode *)node->data; |
870 | 0 | gchar *formtype = jabber_x_data_get_formtype(data); |
871 | 0 | GList *fields = jabber_caps_xdata_get_fields(data); |
872 | | |
873 | | /* append FORM_TYPE's field value to the verification string */ |
874 | 0 | append_escaped_string(context, formtype); |
875 | 0 | g_free(formtype); |
876 | |
|
877 | 0 | while (fields) { |
878 | 0 | GList *value; |
879 | 0 | JabberDataFormField *field = (JabberDataFormField*)fields->data; |
880 | |
|
881 | 0 | if (!purple_strequal(field->var, "FORM_TYPE")) { |
882 | | /* Append the "var" attribute */ |
883 | 0 | append_escaped_string(context, field->var); |
884 | | /* Append <value/> elements' cdata */ |
885 | 0 | for (value = field->values; value; value = value->next) { |
886 | 0 | append_escaped_string(context, value->data); |
887 | 0 | g_free(value->data); |
888 | 0 | } |
889 | 0 | } |
890 | |
|
891 | 0 | g_free(field->var); |
892 | 0 | g_list_free(field->values); |
893 | |
|
894 | 0 | fields = g_list_delete_link(fields, fields); |
895 | 0 | } |
896 | 0 | } |
897 | | |
898 | | /* generate hash */ |
899 | 0 | success = purple_cipher_context_digest(context, checksum_size, |
900 | 0 | checksum, &checksum_size); |
901 | |
|
902 | 0 | purple_cipher_context_destroy(context); |
903 | |
|
904 | 0 | return (success ? purple_base64_encode(checksum, checksum_size) : NULL); |
905 | 0 | } |
906 | | |
907 | 0 | void jabber_caps_calculate_own_hash(JabberStream *js) { |
908 | 0 | JabberCapsClientInfo info; |
909 | 0 | GList *iter = 0; |
910 | 0 | GList *features = 0; |
911 | |
|
912 | 0 | if (!jabber_identities && !jabber_features) { |
913 | | /* This really shouldn't ever happen */ |
914 | 0 | purple_debug_warning("jabber", "No features or identities, cannot calculate own caps hash.\n"); |
915 | 0 | g_free(js->caps_hash); |
916 | 0 | js->caps_hash = NULL; |
917 | 0 | return; |
918 | 0 | } |
919 | | |
920 | | /* build the currently-supported list of features */ |
921 | 0 | if (jabber_features) { |
922 | 0 | for (iter = jabber_features; iter; iter = iter->next) { |
923 | 0 | JabberFeature *feat = iter->data; |
924 | 0 | if(!feat->is_enabled || feat->is_enabled(js, feat->namespace)) { |
925 | 0 | features = g_list_append(features, feat->namespace); |
926 | 0 | } |
927 | 0 | } |
928 | 0 | } |
929 | |
|
930 | 0 | info.features = features; |
931 | | /* TODO: This copy can go away, I think, since jabber_identities |
932 | | * is pre-sorted, so the sort in calculate_hash should be idempotent. |
933 | | * However, I want to test that. --darkrain |
934 | | */ |
935 | 0 | info.identities = g_list_copy(jabber_identities); |
936 | 0 | info.forms = NULL; |
937 | |
|
938 | 0 | g_free(js->caps_hash); |
939 | 0 | js->caps_hash = jabber_caps_calculate_hash(&info, "sha1"); |
940 | 0 | g_list_free(info.identities); |
941 | 0 | g_list_free(info.features); |
942 | 0 | } |
943 | | |
944 | | const gchar* jabber_caps_get_own_hash(JabberStream *js) |
945 | 0 | { |
946 | 0 | if (!js->caps_hash) |
947 | 0 | jabber_caps_calculate_own_hash(js); |
948 | |
|
949 | 0 | return js->caps_hash; |
950 | 0 | } |
951 | | |
952 | | void jabber_caps_broadcast_change() |
953 | 0 | { |
954 | 0 | GList *node, *accounts = purple_accounts_get_all_active(); |
955 | |
|
956 | 0 | for (node = accounts; node; node = node->next) { |
957 | 0 | PurpleAccount *account = node->data; |
958 | 0 | const char *prpl_id = purple_account_get_protocol_id(account); |
959 | 0 | if (purple_strequal("prpl-jabber", prpl_id) && purple_account_is_connected(account)) { |
960 | 0 | PurpleConnection *gc = purple_account_get_connection(account); |
961 | 0 | jabber_presence_send(gc->proto_data, TRUE); |
962 | 0 | } |
963 | 0 | } |
964 | |
|
965 | 0 | g_list_free(accounts); |
966 | 0 | } |
967 | | |