You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
634 lines
17 KiB
634 lines
17 KiB
/* |
|
* caps.c -- Entity Capabilities Cache for mcabber |
|
* |
|
* Copyright (C) 2008-2010 Frank Zschockelt <mcabber@freakysoft.de> |
|
* |
|
* 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, see <http://www.gnu.org/licenses/>. |
|
*/ |
|
|
|
#include <glib.h> |
|
#include <string.h> |
|
#include <sys/stat.h> |
|
#include <unistd.h> |
|
#include <fcntl.h> |
|
|
|
#include "settings.h" |
|
#include "utils.h" |
|
|
|
typedef struct { |
|
char *category; |
|
char *type; |
|
char *name; |
|
} identity_t; |
|
|
|
typedef struct { |
|
GHashTable *fields; |
|
} dataform_t; |
|
|
|
typedef struct { |
|
GHashTable *identities; |
|
GHashTable *features; |
|
GHashTable *forms; |
|
} caps_t; |
|
|
|
static GHashTable *caps_cache = NULL; |
|
|
|
void caps_destroy(gpointer data) |
|
{ |
|
caps_t *c = data; |
|
g_hash_table_destroy(c->identities); |
|
g_hash_table_destroy(c->features); |
|
g_hash_table_destroy(c->forms); |
|
g_free(c); |
|
} |
|
|
|
void identity_destroy(gpointer data) |
|
{ |
|
identity_t *i = data; |
|
g_free(i->category); |
|
g_free(i->type); |
|
g_free(i->name); |
|
g_free(i); |
|
} |
|
|
|
void form_destroy(gpointer data) |
|
{ |
|
dataform_t *f = data; |
|
g_hash_table_destroy(f->fields); |
|
g_free(f); |
|
} |
|
|
|
void field_destroy(gpointer data) |
|
{ |
|
GList *v = data; |
|
g_list_foreach (v, (GFunc) g_free, NULL); |
|
g_list_free (v); |
|
} |
|
|
|
void caps_init(void) |
|
{ |
|
if (!caps_cache) |
|
caps_cache = g_hash_table_new_full(g_str_hash, g_str_equal, |
|
g_free, caps_destroy); |
|
} |
|
|
|
void caps_free(void) |
|
{ |
|
if (caps_cache) { |
|
g_hash_table_destroy(caps_cache); |
|
caps_cache = NULL; |
|
} |
|
} |
|
|
|
void caps_add(const char *hash) |
|
{ |
|
if (!hash) |
|
return; |
|
caps_t *c = g_new0(caps_t, 1); |
|
c->features = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); |
|
c->identities = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, identity_destroy); |
|
c->forms = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, form_destroy); |
|
g_hash_table_replace(caps_cache, g_strdup(hash), c); |
|
} |
|
|
|
void caps_remove(const char *hash) |
|
{ |
|
if (!hash) |
|
return; |
|
g_hash_table_remove(caps_cache, hash); |
|
} |
|
|
|
/* if hash is not verified, this will bind capabilities set only with bare jid */ |
|
void caps_move_to_local(char *hash, char *bjid) |
|
{ |
|
char *orig_hash; |
|
caps_t *c = NULL; |
|
if (!hash || !bjid) |
|
return; |
|
g_hash_table_lookup_extended(caps_cache, hash, (gpointer*)&orig_hash, (gpointer*)&c); |
|
if (c) { |
|
g_hash_table_steal(caps_cache, hash); |
|
g_free(orig_hash); |
|
g_hash_table_replace(caps_cache, g_strdup_printf("%s/#%s", bjid, hash), c); |
|
// solidus is guaranteed to never appear in bare jid |
|
// hash will not appear in base64 encoded hash |
|
// sequence "/#" is deterministic separator, and allows to identify local cache entry |
|
} |
|
} |
|
|
|
/*if bjid is NULL, it will check only verified hashes */ |
|
int caps_has_hash(const char *hash, const char *bjid) |
|
{ |
|
caps_t *c = NULL; |
|
if (!hash) |
|
return 0; |
|
c = g_hash_table_lookup(caps_cache, hash); |
|
if (!c && bjid) { |
|
char *key = g_strdup_printf("%s/#%s", bjid, hash); |
|
c = g_hash_table_lookup(caps_cache, key); |
|
g_free(key); |
|
} |
|
return (c != NULL); |
|
} |
|
|
|
void caps_add_identity(const char *hash, |
|
const char *category, |
|
const char *name, |
|
const char *type, |
|
const char *lang) |
|
{ |
|
caps_t *c; |
|
if (!hash || !category || !type) |
|
return; |
|
if (!lang) |
|
lang = ""; |
|
|
|
c = g_hash_table_lookup(caps_cache, hash); |
|
if (c) { |
|
identity_t *i = g_new0(identity_t, 1); |
|
|
|
i->category = g_strdup(category); |
|
i->name = g_strdup(name); |
|
i->type = g_strdup(type); |
|
g_hash_table_replace(c->identities, g_strdup(lang), i); |
|
} |
|
} |
|
|
|
void caps_set_identity(char *hash, |
|
const char *category, |
|
const char *name, |
|
const char *type) |
|
{ |
|
caps_add_identity(hash, category, name, type, NULL); |
|
} |
|
|
|
void caps_add_dataform(const char *hash, const char *formtype) |
|
{ |
|
caps_t *c; |
|
if (!formtype) |
|
return; |
|
c = g_hash_table_lookup(caps_cache, hash); |
|
if (c) { |
|
dataform_t *d = g_new0(dataform_t, 1); |
|
char *f = g_strdup(formtype); |
|
|
|
d->fields = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, field_destroy); |
|
g_hash_table_replace(c->forms, f, d); |
|
} |
|
} |
|
|
|
gint _strcmp_sort(gconstpointer a, gconstpointer b) |
|
{ |
|
return g_strcmp0(a, b); |
|
} |
|
|
|
void caps_add_dataform_field(const char *hash, const char *formtype, |
|
const char *field, const char *value) |
|
{ |
|
caps_t *c; |
|
if (!formtype || !field || !value) |
|
return; |
|
c = g_hash_table_lookup(caps_cache, hash); |
|
if (c) { |
|
dataform_t *d; |
|
d = g_hash_table_lookup(c->forms, formtype); |
|
if (d) { |
|
gpointer key, val; |
|
char *f; |
|
GList *v = NULL; |
|
if (g_hash_table_lookup_extended(d->fields, field, &key, &val)) { |
|
g_hash_table_steal(d->fields, field); |
|
g_free(key); |
|
v = val; |
|
} |
|
f = g_strdup(field); |
|
v = g_list_insert_sorted(v, g_strdup(value), _strcmp_sort); |
|
g_hash_table_replace(d->fields, f, v); |
|
} |
|
} |
|
} |
|
|
|
void caps_add_feature(const char *hash, const char *feature) |
|
{ |
|
caps_t *c; |
|
if (!hash || !feature) |
|
return; |
|
c = g_hash_table_lookup(caps_cache, hash); |
|
if (c) { |
|
char *f = g_strdup(feature); |
|
g_hash_table_replace(c->features, f, f); |
|
} |
|
} |
|
|
|
/* If hash is verified, then bare jid is ignored. |
|
* If there is no globally verified hash, and bare jid is not null, |
|
* then local storage for that jid will be checked */ |
|
int caps_has_feature(const char *hash, char *feature, char *bjid) |
|
{ |
|
caps_t *c = NULL; |
|
if (!hash || !feature) |
|
return 0; |
|
c = g_hash_table_lookup(caps_cache, hash); |
|
if (!c && bjid) { |
|
char *key = g_strdup_printf("%s/#%s", bjid, hash); |
|
c = g_hash_table_lookup(caps_cache, key); |
|
g_free(key); |
|
} |
|
if (c) |
|
return (g_hash_table_lookup(c->features, feature) != NULL); |
|
return 0; |
|
} |
|
|
|
static GFunc _foreach_function; |
|
|
|
void _caps_foreach_helper(gpointer key, gpointer value, gpointer user_data) |
|
{ |
|
// GFunc func = (GFunc)user_data; |
|
_foreach_function(value, user_data); |
|
} |
|
|
|
void caps_foreach_feature(const char *hash, GFunc func, gpointer user_data) |
|
{ |
|
caps_t *c; |
|
if (!hash) |
|
return; |
|
c = g_hash_table_lookup(caps_cache, hash); |
|
if (!c) |
|
return; |
|
_foreach_function = func; |
|
g_hash_table_foreach(c->features, _caps_foreach_helper, user_data); |
|
} |
|
|
|
// Generates the sha1 hash for the special capability "" and returns it |
|
const char *caps_generate(void) |
|
{ |
|
GList *features, *langs; |
|
GChecksum *sha1; |
|
guint8 digest[20]; |
|
gsize digest_size = 20; |
|
gchar *hash, *old_hash = NULL; |
|
caps_t *old_caps, *c; |
|
gpointer key; |
|
|
|
if (!g_hash_table_lookup_extended(caps_cache, "", &key, (gpointer *)&c)) |
|
return NULL; |
|
|
|
g_hash_table_steal(caps_cache, ""); |
|
g_free(key); |
|
|
|
sha1 = g_checksum_new(G_CHECKSUM_SHA1); |
|
|
|
langs = g_hash_table_get_keys(c->identities); |
|
langs = g_list_sort(langs, _strcmp_sort); |
|
{ |
|
identity_t *i; |
|
GList *lang; |
|
char *identity_S; |
|
for (lang=langs; lang; lang=lang->next) { |
|
i = g_hash_table_lookup(c->identities, lang->data); |
|
identity_S = g_strdup_printf("%s/%s/%s/%s<", i->category, i->type, |
|
(char *)lang->data, i->name ? i->name : ""); |
|
g_checksum_update(sha1, (guchar *)identity_S, -1); |
|
g_free(identity_S); |
|
} |
|
} |
|
g_list_free(langs); |
|
|
|
features = g_hash_table_get_values(c->features); |
|
features = g_list_sort(features, _strcmp_sort); |
|
{ |
|
GList *feature; |
|
for (feature=features; feature; feature=feature->next) { |
|
g_checksum_update(sha1, feature->data, -1); |
|
g_checksum_update(sha1, (guchar *)"<", -1); |
|
} |
|
} |
|
g_list_free(features); |
|
|
|
g_checksum_get_digest(sha1, digest, &digest_size); |
|
hash = g_base64_encode(digest, digest_size); |
|
g_checksum_free(sha1); |
|
g_hash_table_lookup_extended(caps_cache, hash, |
|
(gpointer *)&old_hash, (gpointer *)&old_caps); |
|
g_hash_table_insert(caps_cache, hash, c); |
|
if (old_hash) |
|
return old_hash; |
|
else |
|
return hash; |
|
} |
|
|
|
gboolean caps_verify(const char *hash, char *function) |
|
{ |
|
GList *features, *langs, *forms; |
|
GChecksum *checksum; |
|
guint8 digest[20]; |
|
gsize digest_size = 20; |
|
gchar *local_hash; |
|
gboolean match = FALSE; |
|
caps_t *c = g_hash_table_lookup(caps_cache, hash); |
|
|
|
if (!g_strcmp0(function, "sha-1")) { |
|
checksum = g_checksum_new(G_CHECKSUM_SHA1); |
|
} else if (!g_strcmp0(function, "md5")) { |
|
checksum = g_checksum_new(G_CHECKSUM_MD5); |
|
digest_size = 16; |
|
} else |
|
return FALSE; |
|
|
|
langs = g_hash_table_get_keys(c->identities); |
|
langs = g_list_sort(langs, _strcmp_sort); |
|
{ |
|
identity_t *i; |
|
GList *lang; |
|
char *identity_S; |
|
for (lang=langs; lang; lang=lang->next) { |
|
i = g_hash_table_lookup(c->identities, lang->data); |
|
identity_S = g_strdup_printf("%s/%s/%s/%s<", i->category, i->type, |
|
(char *)lang->data, i->name ? i->name : ""); |
|
g_checksum_update(checksum, (guchar *)identity_S, -1); |
|
g_free(identity_S); |
|
} |
|
} |
|
g_list_free(langs); |
|
|
|
features = g_hash_table_get_values(c->features); |
|
features = g_list_sort(features, _strcmp_sort); |
|
{ |
|
GList *feature; |
|
for (feature=features; feature; feature=feature->next) { |
|
g_checksum_update(checksum, feature->data, -1); |
|
g_checksum_update(checksum, (guchar *)"<", -1); |
|
} |
|
} |
|
g_list_free(features); |
|
|
|
forms = g_hash_table_get_keys(c->forms); |
|
forms = g_list_sort(forms, _strcmp_sort); |
|
{ |
|
dataform_t *d; |
|
GList *form, *fields; |
|
for (form=forms; form; form=form->next) { |
|
d = g_hash_table_lookup(c->forms, form->data); |
|
g_checksum_update(checksum, form->data, -1); |
|
g_checksum_update(checksum, (guchar *)"<", -1); |
|
fields = g_hash_table_get_keys(d->fields); |
|
fields = g_list_sort(fields, _strcmp_sort); |
|
{ |
|
GList *field; |
|
GList *values; |
|
for (field=fields; field; field=field->next) { |
|
g_checksum_update(checksum, field->data, -1); |
|
g_checksum_update(checksum, (guchar *)"<", -1); |
|
values = g_hash_table_lookup(d->fields, field->data); |
|
{ |
|
GList *value; |
|
for (value=values; value; value=value->next) { |
|
g_checksum_update(checksum, value->data, -1); |
|
g_checksum_update(checksum, (guchar *)"<", -1); |
|
} |
|
} |
|
} |
|
} |
|
g_list_free(fields); |
|
} |
|
} |
|
g_list_free(forms); |
|
|
|
g_checksum_get_digest(checksum, digest, &digest_size); |
|
local_hash = g_base64_encode(digest, digest_size); |
|
g_checksum_free(checksum); |
|
|
|
match = !g_strcmp0(hash, local_hash); |
|
|
|
g_free(local_hash); |
|
return match; |
|
} |
|
|
|
static gchar* caps_get_filename(const char* hash) |
|
{ |
|
gchar *hash_fs; |
|
gchar *dir = (gchar *) settings_opt_get ("caps_directory"); |
|
gchar *file = NULL; |
|
|
|
if (!dir) |
|
goto caps_filename_return; |
|
|
|
hash_fs = g_strdup (hash); |
|
{ |
|
const gchar *valid_fs = |
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+="; |
|
g_strcanon(hash_fs, valid_fs, '-'); |
|
} |
|
|
|
dir = expand_filename (dir); |
|
file = g_strdup_printf ("%s/%s.ini", dir, hash_fs); |
|
g_free(dir); |
|
g_free(hash_fs); |
|
|
|
caps_filename_return: |
|
return file; |
|
} |
|
|
|
/* Store capabilities set in GKeyFile. To be used with verified hashes only */ |
|
void caps_copy_to_persistent(const char* hash, char* xml) |
|
{ |
|
gchar *file; |
|
GList *features, *langs, *forms; |
|
GKeyFile *key_file; |
|
caps_t *c; |
|
int fd; |
|
|
|
g_free (xml); |
|
|
|
c = g_hash_table_lookup (caps_cache, hash); |
|
if (!c) |
|
goto caps_copy_return; |
|
|
|
file = caps_get_filename (hash); |
|
if (!file) |
|
goto caps_copy_return; |
|
|
|
fd = open (file, O_WRONLY|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR); |
|
if (fd == -1) |
|
goto caps_copy_exists; |
|
|
|
key_file = g_key_file_new (); |
|
g_key_file_set_comment (key_file, NULL, NULL, |
|
"This is autogenerated file. Please do not modify.", |
|
NULL); |
|
|
|
langs = g_hash_table_get_keys (c->identities); |
|
{ |
|
identity_t *i; |
|
GList *lang; |
|
gchar *group; |
|
for (lang=langs; lang; lang=lang->next) { |
|
i = g_hash_table_lookup (c->identities, lang->data); |
|
group = g_strdup_printf("identity_%s", (gchar *)lang->data); |
|
g_key_file_set_string (key_file, group, "category", i->category); |
|
g_key_file_set_string (key_file, group, "type", i->type); |
|
g_key_file_set_string (key_file, group, "name", i->name); |
|
g_free (group); |
|
} |
|
} |
|
g_list_free (langs); |
|
|
|
features = g_hash_table_get_values (c->features); |
|
{ |
|
GList *feature; |
|
gchar **string_list; |
|
gint i; |
|
|
|
i = g_list_length (features); |
|
string_list = g_new (gchar*, i + 1); |
|
i = 0; |
|
for (feature=features; feature; feature=feature->next) { |
|
string_list[i] = g_strdup(feature->data); |
|
++i; |
|
} |
|
string_list[i] = NULL; |
|
|
|
g_key_file_set_string_list (key_file, "features", "features", |
|
(const gchar**)string_list, i); |
|
g_strfreev (string_list); |
|
} |
|
g_list_free (features); |
|
|
|
forms = g_hash_table_get_keys(c->forms); |
|
{ |
|
dataform_t *d; |
|
GList *form, *fields; |
|
gchar *group; |
|
for (form=forms; form; form=form->next) { |
|
d = g_hash_table_lookup (c->forms, form->data); |
|
group = g_strdup_printf ("form_%s", (gchar *)form->data); |
|
fields = g_hash_table_get_keys(d->fields); |
|
{ |
|
GList *field; |
|
GList *values; |
|
for (field=fields; field; field=field->next) { |
|
values = g_hash_table_lookup (d->fields, field->data); |
|
{ |
|
GList *value; |
|
gchar **string_list; |
|
gint i; |
|
i = g_list_length (values); |
|
string_list = g_new (gchar*, i + 1); |
|
i = 0; |
|
for (value=values; value; value=value->next) { |
|
string_list[i] = g_strdup(value->data); |
|
++i; |
|
} |
|
string_list[i] = NULL; |
|
|
|
g_key_file_set_string_list (key_file, group, field->data, |
|
(const gchar**)string_list, i); |
|
|
|
g_strfreev (string_list); |
|
} |
|
} |
|
} |
|
g_list_free(fields); |
|
g_free (group); |
|
} |
|
} |
|
g_list_free (forms); |
|
|
|
{ |
|
gchar *data; |
|
gsize length; |
|
data = g_key_file_to_data (key_file, &length, NULL); |
|
write (fd, data, length); |
|
g_free(data); |
|
close (fd); |
|
} |
|
|
|
g_key_file_free(key_file); |
|
caps_copy_exists: |
|
g_free(file); |
|
caps_copy_return: |
|
return; |
|
} |
|
|
|
/* Restore capabilities from GKeyFile. Hash is not verified afterwards */ |
|
gboolean caps_restore_from_persistent (const char* hash) |
|
{ |
|
gchar *file; |
|
GKeyFile *key_file; |
|
gchar **groups, **group; |
|
gboolean restored = FALSE; |
|
|
|
file = caps_get_filename (hash); |
|
if (!file) |
|
goto caps_restore_no_file; |
|
|
|
key_file = g_key_file_new (); |
|
if (!g_key_file_load_from_file (key_file, file, G_KEY_FILE_NONE, NULL)) |
|
goto caps_restore_bad_file; |
|
|
|
caps_add(hash); |
|
|
|
groups = g_key_file_get_groups (key_file, NULL); |
|
for (group = groups; *group; ++group) { |
|
if (!g_strcmp0(*group, "features")) { |
|
gchar **features, **feature; |
|
features = g_key_file_get_string_list (key_file, *group, "features", |
|
NULL, NULL); |
|
for (feature = features; *feature; ++feature) { |
|
caps_add_feature(hash, *feature); |
|
} |
|
|
|
g_strfreev (features); |
|
} else if (g_str_has_prefix (*group, "identity_")) { |
|
gchar *category, *type, *name, *lang; |
|
|
|
category = g_key_file_get_string(key_file, *group, "category", NULL); |
|
type = g_key_file_get_string(key_file, *group, "type", NULL); |
|
name = g_key_file_get_string(key_file, *group, "name", NULL); |
|
lang = *group + 9; /* "identity_" */ |
|
|
|
caps_add_identity(hash, category, name, type, lang); |
|
g_free(category); |
|
g_free(type); |
|
g_free(name); |
|
} else if (g_str_has_prefix (*group, "form_")) { |
|
gchar *formtype; |
|
gchar **fields, **field; |
|
formtype = *group + 5; /* "form_" */ |
|
caps_add_dataform (hash, formtype); |
|
|
|
fields = g_key_file_get_keys(key_file, *group, NULL, NULL); |
|
for (field = fields; *field; ++field) { |
|
gchar **values, **value; |
|
values = g_key_file_get_string_list (key_file, *group, *field, |
|
NULL, NULL); |
|
for (value = values; *value; ++value) { |
|
caps_add_dataform_field (hash, formtype, *field, *value); |
|
} |
|
g_strfreev (values); |
|
} |
|
g_strfreev (fields); |
|
} |
|
} |
|
g_strfreev(groups); |
|
restored = TRUE; |
|
|
|
caps_restore_bad_file: |
|
g_key_file_free (key_file); |
|
g_free (file); |
|
caps_restore_no_file: |
|
return restored; |
|
} |
|
|
|
/* vim: set expandtab cindent cinoptions=>2\:2(0 sw=2 ts=2: For Vim users... */
|
|
|