view libpurple/protocols/jabber/jutil.c @ 31686:167ea4b4765c

Patch from Zac West which performs periodic WHO updates on IRC channel participants in order to track 'away' status, userhost, and real name. Plucked from a series of im.pidgin.adium commits.
author Evan Schoenberg <evan.s@dreskin.net>
date Mon, 21 Feb 2011 01:25:28 +0000
parents a8cc50c2279f
children b44e3c2e83e1
line wrap: on
line source

/*
 * purple - Jabber Protocol Plugin
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * 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 "account.h"
#include "cipher.h"
#include "conversation.h"
#include "debug.h"
#include "server.h"
#include "util.h"
#include "xmlnode.h"

#include "chat.h"
#include "presence.h"
#include "jutil.h"

#ifdef USE_IDN
#include <idna.h>
#include <stringprep.h>
static char idn_buffer[1024];
#endif

#ifdef USE_IDN
static gboolean jabber_nodeprep(char *str, size_t buflen)
{
	return stringprep_xmpp_nodeprep(str, buflen) == STRINGPREP_OK;
}

static gboolean jabber_resourceprep(char *str, size_t buflen)
{
	return stringprep_xmpp_resourceprep(str, buflen) == STRINGPREP_OK;
}

static JabberID*
jabber_idn_validate(const char *str, const char *at, const char *slash,
                    const char *null)
{
	const char *node = NULL;
	const char *domain = NULL;
	const char *resource = NULL;
	int node_len = 0;
	int domain_len = 0;
	int resource_len = 0;
	char *out;
	JabberID *jid;

	/* Ensure no parts are > 1023 bytes */
	if (at) {
		node = str;
		node_len = at - str;

		domain = at + 1;
		if (slash) {
			domain_len = slash - (at + 1);
			resource = slash + 1;
			resource_len = null - (slash + 1);
		} else {
			domain_len = null - (at + 1);
		}
	} else {
		domain = str;

		if (slash) {
			domain_len = slash - str;
			resource = slash;
			resource_len = null - (slash + 1);
		} else {
			domain_len = null - (str + 1);
		}
	}

	if (node && node_len > 1023)
		return NULL;
	if (domain_len > 1023)
		return NULL;
	if (resource && resource_len > 1023)
		return NULL;

	jid = g_new0(JabberID, 1);

	if (node) {
		strncpy(idn_buffer, node, node_len);
		idn_buffer[node_len] = '\0';

		if (!jabber_nodeprep(idn_buffer, sizeof(idn_buffer))) {
			jabber_id_free(jid);
			jid = NULL;
			goto out;
		}

		jid->node = g_strdup(idn_buffer);
	}

	/* domain *must* be here */
	strncpy(idn_buffer, domain, domain_len);
	idn_buffer[domain_len] = '\0';
	if (domain[0] == '[') { /* IPv6 address */
		gboolean valid = FALSE;

		if (idn_buffer[domain_len - 1] == ']') {
			idn_buffer[domain_len - 1] = '\0';
			valid = purple_ipv6_address_is_valid(idn_buffer + 1);
		}

		if (!valid) {
			jabber_id_free(jid);
			jid = NULL;
			goto out;
		}
	} else {
		/* Apply nameprep */
		if (stringprep_nameprep(idn_buffer, sizeof(idn_buffer)) != STRINGPREP_OK) {
			jabber_id_free(jid);
			jid = NULL;
			goto out;
		}

		/* And now ToASCII */
		if (idna_to_ascii_8z(idn_buffer, &out, IDNA_USE_STD3_ASCII_RULES) != IDNA_SUCCESS) {
			jabber_id_free(jid);
			jid = NULL;
			goto out;
		}

		/* This *MUST* be freed using 'free', not 'g_free' */
		free(out);
		jid->domain = g_strdup(idn_buffer);
	}

	if (resource) {
		strncpy(idn_buffer, resource, resource_len);
		idn_buffer[resource_len] = '\0';

		if (!jabber_resourceprep(idn_buffer, sizeof(idn_buffer))) {
			jabber_id_free(jid);
			jid = NULL;
			goto out;
		} else
			jid->resource = g_strdup(idn_buffer);
	}

out:
	return jid;
}

#endif /* USE_IDN */

gboolean jabber_nodeprep_validate(const char *str)
{
#ifdef USE_IDN
	gboolean result;
#else
	const char *c;
#endif

	if(!str)
		return TRUE;

	if(strlen(str) > 1023)
		return FALSE;

#ifdef USE_IDN
	strncpy(idn_buffer, str, sizeof(idn_buffer) - 1);
	idn_buffer[sizeof(idn_buffer) - 1] = '\0';
	result = jabber_nodeprep(idn_buffer, sizeof(idn_buffer));
	return result;
#else /* USE_IDN */
	c = str;
	while(c && *c) {
		gunichar ch = g_utf8_get_char(c);
		if(ch == '\"' || ch == '&' || ch == '\'' || ch == '/' || ch == ':' ||
				ch == '<' || ch == '>' || ch == '@' || !g_unichar_isgraph(ch)) {
			return FALSE;
		}
		c = g_utf8_next_char(c);
	}

	return TRUE;
#endif /* USE_IDN */
}

gboolean jabber_domain_validate(const char *str)
{
	const char *c;
	size_t len;

	if(!str)
		return TRUE;

	len = strlen(str);
	if (len > 1023)
		return FALSE;

	c = str;

	if (*c == '[') {
		/* Check if str is a valid IPv6 identifier */
		gboolean valid = FALSE;

		if (*(c + len - 1) != ']')
			return FALSE;

		/* Ugly, but in-place */
		*(gchar *)(c + len - 1) = '\0';
		valid = purple_ipv6_address_is_valid(c + 1);
		*(gchar *)(c + len - 1) = ']';

		return valid;
	}

	while(c && *c) {
		gunichar ch = g_utf8_get_char(c);
		/* The list of characters allowed in domain names is pretty small */
		if ((ch <= 0x7F && !( (ch >= 'a' && ch <= 'z')
				|| (ch >= '0' && ch <= '9')
				|| (ch >= 'A' && ch <= 'Z')
				|| ch == '.'
				|| ch == '-' )) || (ch >= 0x80 && !g_unichar_isgraph(ch)))
			return FALSE;

		c = g_utf8_next_char(c);
	}

	return TRUE;
}

gboolean jabber_resourceprep_validate(const char *str)
{
#ifdef USE_IDN
	gboolean result;
#else
	const char *c;
#endif

	if(!str)
		return TRUE;

	if(strlen(str) > 1023)
		return FALSE;

#ifdef USE_IDN
	strncpy(idn_buffer, str, sizeof(idn_buffer) - 1);
	idn_buffer[sizeof(idn_buffer) - 1] = '\0';
	result = jabber_resourceprep(idn_buffer, sizeof(idn_buffer));
	return result;
#else /* USE_IDN */
	c = str;
	while(c && *c) {
		gunichar ch = g_utf8_get_char(c);
		if(!g_unichar_isgraph(ch) && ch != ' ')
			return FALSE;

		c = g_utf8_next_char(c);
	}

	return TRUE;
#endif /* USE_IDN */
}

char *jabber_saslprep(const char *in)
{
#ifdef USE_IDN
	char *out;

	g_return_val_if_fail(in != NULL, NULL);
	g_return_val_if_fail(strlen(in) <= sizeof(idn_buffer) - 1, NULL);

	strncpy(idn_buffer, in, sizeof(idn_buffer) - 1);
	idn_buffer[sizeof(idn_buffer) - 1] = '\0';

	if (STRINGPREP_OK != stringprep(idn_buffer, sizeof(idn_buffer), 0,
	                                stringprep_saslprep)) {
		memset(idn_buffer, 0, sizeof(idn_buffer));
		return NULL;
	}

	out = g_strdup(idn_buffer);
	memset(idn_buffer, 0, sizeof(idn_buffer));
	return out;
#else /* USE_IDN */
	/* TODO: Something better than disallowing all non-ASCII characters */
	/* TODO: Is this even correct? */
	const guchar *c;

	c = (const guchar *)in;
	for ( ; *c; ++c) {
		if (*c > 0x7f || /* Non-ASCII characters */
				*c == 0x7f || /* ASCII Delete character */
				(*c < 0x20 && *c != '\t' && *c != '\n' && *c != '\r'))
					/* ASCII control characters */
			return NULL;
	}

	return g_strdup(in);
#endif /* USE_IDN */
}

static JabberID*
jabber_id_new_internal(const char *str, gboolean allow_terminating_slash)
{
	const char *at = NULL;
	const char *slash = NULL;
	const char *c;
	gboolean needs_validation = FALSE;
#if 0
	gboolean node_is_required = FALSE;
#endif
#ifndef USE_IDN
	char *node = NULL;
	char *domain;
#endif
	JabberID *jid;

	if (!str)
		return NULL;

	for (c = str; *c != '\0'; c++)
	{
		switch (*c) {
			case '@':
				if (!slash) {
					if (at) {
						/* Multiple @'s in the node/domain portion, not a valid JID! */
						return NULL;
					}
					if (c == str) {
						/* JIDs cannot start with @ */
						return NULL;
					}
					if (c[1] == '\0') {
						/* JIDs cannot end with @ */
						return NULL;
					}
					at = c;
				}
				break;

			case '/':
				if (!slash) {
					if (c == str) {
						/* JIDs cannot start with / */
						return NULL;
					}
					if (c[1] == '\0' && !allow_terminating_slash) {
						/* JIDs cannot end with / */
						return NULL;
					}
					slash = c;
				}
				break;

			default:
				/* characters allowed everywhere */
				if ((*c >= 'a' && *c <= 'z')
						|| (*c >= '0' && *c <= '9')
						|| (*c >= 'A' && *c <= 'Z')
						|| *c == '.' || *c == '-')
					/* We're good */
					break;

#if 0
				if (slash != NULL) {
					/* characters allowed only in the resource */
					if (implement_me)
						/* We're good */
						break;
				}

				/* characters allowed only in the node */
				if (implement_me) {
					/*
					 * Ok, this character is valid, but only if it's a part
					 * of the node and not the domain.  But we don't know
					 * if "c" is a part of the node or the domain until after
					 * we've found the @.  So set a flag for now and check
					 * that we found an @ later.
					 */
					node_is_required = TRUE;
					break;
				}
#endif

				/*
				 * Hmm, this character is a bit more exotic.  Better fall
				 * back to using the more expensive UTF-8 compliant
				 * stringprep functions.
				 */
				needs_validation = TRUE;
				break;
		}
	}

#if 0
	if (node_is_required && at == NULL)
		/* Found invalid characters in the domain */
		return NULL;
#endif

	if (!needs_validation) {
		/* JID is made of only ASCII characters--just lowercase and return */
		jid = g_new0(JabberID, 1);

		if (at) {
			jid->node = g_ascii_strdown(str, at - str);
			if (slash) {
				jid->domain = g_ascii_strdown(at + 1, slash - (at + 1));
				if (*(slash + 1))
					jid->resource = g_strdup(slash + 1);
			} else {
				jid->domain = g_ascii_strdown(at + 1, -1);
			}
		} else {
			if (slash) {
				jid->domain = g_ascii_strdown(str, slash - str);
				if (*(slash + 1))
					jid->resource = g_strdup(slash + 1);
			} else {
				jid->domain = g_ascii_strdown(str, -1);
			}
		}
		return jid;
	}

	/*
	 * If we get here, there are some non-ASCII chars in the string, so
	 * we'll need to validate it, normalize, and finally do a full jabber
	 * nodeprep on the jid.
	 */

	if (!g_utf8_validate(str, -1, NULL))
		return NULL;

#ifdef USE_IDN
	return jabber_idn_validate(str, at, slash, c /* points to the null */);
#else /* USE_IDN */

	jid = g_new0(JabberID, 1);

	/* normalization */
	if(at) {
		node = g_utf8_casefold(str, at-str);
		if(slash) {
			domain = g_utf8_casefold(at+1, slash-(at+1));
			if (*(slash + 1))
				jid->resource = g_utf8_normalize(slash+1, -1, G_NORMALIZE_NFKC);
		} else {
			domain = g_utf8_casefold(at+1, -1);
		}
	} else {
		if(slash) {
			domain = g_utf8_casefold(str, slash-str);
			if (*(slash + 1))
				jid->resource = g_utf8_normalize(slash+1, -1, G_NORMALIZE_NFKC);
		} else {
			domain = g_utf8_casefold(str, -1);
		}
	}

	if (node) {
		jid->node = g_utf8_normalize(node, -1, G_NORMALIZE_NFKC);
		g_free(node);
	}

	if (domain) {
		jid->domain = g_utf8_normalize(domain, -1, G_NORMALIZE_NFKC);
		g_free(domain);
	}

	/* and finally the jabber nodeprep */
	if(!jabber_nodeprep_validate(jid->node) ||
			!jabber_domain_validate(jid->domain) ||
			!jabber_resourceprep_validate(jid->resource)) {
		jabber_id_free(jid);
		return NULL;
	}

	return jid;
#endif /* USE_IDN */
}

void
jabber_id_free(JabberID *jid)
{
	if(jid) {
		if(jid->node)
			g_free(jid->node);
		if(jid->domain)
			g_free(jid->domain);
		if(jid->resource)
			g_free(jid->resource);
		g_free(jid);
	}
}

char *jabber_get_domain(const char *in)
{
	JabberID *jid = jabber_id_new(in);
	char *out;

	if (!jid)
		return NULL;

	out = g_strdup(jid->domain);
	jabber_id_free(jid);

	return out;
}

char *jabber_get_resource(const char *in)
{
	JabberID *jid = jabber_id_new(in);
	char *out;

	if(!jid)
		return NULL;

	out = g_strdup(jid->resource);
	jabber_id_free(jid);

	return out;
}

char *
jabber_get_bare_jid(const char *in)
{
	JabberID *jid = jabber_id_new(in);
	char *out;

	if (!jid)
		return NULL;
	out = jabber_id_get_bare_jid(jid);
	jabber_id_free(jid);

	return out;
}

char *
jabber_id_get_bare_jid(const JabberID *jid)
{
	g_return_val_if_fail(jid != NULL, NULL);

	return g_strconcat(jid->node ? jid->node : "",
	                   jid->node ? "@" : "",
	                   jid->domain,
	                   NULL);
}

gboolean
jabber_jid_is_domain(const char *jid)
{
	const char *c;

	for (c = jid; *c; ++c) {
		if (*c == '@' || *c == '/')
			return FALSE;
	}

	return TRUE;
}


JabberID *
jabber_id_new(const char *str)
{
	return jabber_id_new_internal(str, FALSE);
}

const char *jabber_normalize(const PurpleAccount *account, const char *in)
{
	PurpleConnection *gc = account ? account->gc : NULL;
	JabberStream *js = gc ? gc->proto_data : NULL;
	static char buf[3072]; /* maximum legal length of a jabber jid */
	JabberID *jid;

	jid = jabber_id_new_internal(in, TRUE);
	if(!jid)
		return NULL;

	if(js && jid->node && jid->resource &&
			jabber_chat_find(js, jid->node, jid->domain))
		g_snprintf(buf, sizeof(buf), "%s@%s/%s", jid->node, jid->domain,
				jid->resource);
	else
		g_snprintf(buf, sizeof(buf), "%s%s%s", jid->node ? jid->node : "",
				jid->node ? "@" : "", jid->domain);

	jabber_id_free(jid);

	return buf;
}

gboolean
jabber_is_own_server(JabberStream *js, const char *str)
{
	JabberID *jid;
	gboolean equal;

	if (str == NULL)
		return FALSE;

	g_return_val_if_fail(*str != '\0', FALSE);

	jid = jabber_id_new(str);
	if (!jid)
		return FALSE;

	equal = (jid->node == NULL &&
	         g_str_equal(jid->domain, js->user->domain) &&
	         jid->resource == NULL);
	jabber_id_free(jid);
	return equal;
}

gboolean
jabber_is_own_account(JabberStream *js, const char *str)
{
	JabberID *jid;
	gboolean equal;

	if (str == NULL)
		return TRUE;

	g_return_val_if_fail(*str != '\0', FALSE);

	jid = jabber_id_new(str);
	if (!jid)
		return FALSE;

	equal = (purple_strequal(jid->node, js->user->node) &&
	         g_str_equal(jid->domain, js->user->domain) &&
	         (jid->resource == NULL ||
	             g_str_equal(jid->resource, js->user->resource)));
	jabber_id_free(jid);
	return equal;
}

static const struct {
		const char *status_id; /* link to core */
		const char *show; /* The show child's cdata in a presence stanza */
		const char *readable; /* readable representation */
		JabberBuddyState state;
} jabber_statuses[] = {
	{ "offline",       NULL,   N_("Offline"),        JABBER_BUDDY_STATE_UNAVAILABLE },
	{ "available",     NULL,   N_("Available"),      JABBER_BUDDY_STATE_ONLINE},
	{ "freeforchat",   "chat", N_("Chatty"),         JABBER_BUDDY_STATE_CHAT },
	{ "away",          "away", N_("Away"),           JABBER_BUDDY_STATE_AWAY },
	{ "extended_away", "xa",   N_("Extended Away"),  JABBER_BUDDY_STATE_XA },
	{ "dnd",           "dnd",  N_("Do Not Disturb"), JABBER_BUDDY_STATE_DND },
	{ "error",         NULL,   N_("Error"),          JABBER_BUDDY_STATE_ERROR }
};

const char *
jabber_buddy_state_get_name(const JabberBuddyState state)
{
	int i;
	for (i = 0; i < G_N_ELEMENTS(jabber_statuses); ++i)
		if (jabber_statuses[i].state == state)
			return _(jabber_statuses[i].readable);

	return _("Unknown");
}

JabberBuddyState
jabber_buddy_status_id_get_state(const char *id)
{
	int i;
	if (!id)
		return JABBER_BUDDY_STATE_UNKNOWN;

	for (i = 0; i < G_N_ELEMENTS(jabber_statuses); ++i)
		if (g_str_equal(id, jabber_statuses[i].status_id))
			return jabber_statuses[i].state;

	return JABBER_BUDDY_STATE_UNKNOWN;
}

JabberBuddyState jabber_buddy_show_get_state(const char *id)
{
	int i;

	g_return_val_if_fail(id != NULL, JABBER_BUDDY_STATE_UNKNOWN);

	for (i = 0; i < G_N_ELEMENTS(jabber_statuses); ++i)
		if (jabber_statuses[i].show && g_str_equal(id, jabber_statuses[i].show))
			return jabber_statuses[i].state;

	purple_debug_warning("jabber", "Invalid value of presence <show/> "
	                     "attribute: %s\n", id);
	return JABBER_BUDDY_STATE_UNKNOWN;
}

const char *
jabber_buddy_state_get_show(JabberBuddyState state)
{
	int i;
	for (i = 0; i < G_N_ELEMENTS(jabber_statuses); ++i)
		if (state == jabber_statuses[i].state)
			return jabber_statuses[i].show;

	return NULL;
}

const char *
jabber_buddy_state_get_status_id(JabberBuddyState state)
{
	int i;
	for (i = 0; i < G_N_ELEMENTS(jabber_statuses); ++i)
		if (state == jabber_statuses[i].state)
			return jabber_statuses[i].status_id;

	return NULL;
}

char *
jabber_calculate_data_hash(gconstpointer data, size_t len,
    const gchar *hash_algo)
{
	PurpleCipherContext *context;
	static gchar digest[129]; /* 512 bits hex + \0 */

	context = purple_cipher_context_new_by_name(hash_algo, NULL);
	if (context == NULL)
	{
		purple_debug_error("jabber", "Could not find %s cipher\n", hash_algo);
		g_return_val_if_reached(NULL);
	}

	/* Hash the data */
	purple_cipher_context_append(context, data, len);
	if (!purple_cipher_context_digest_to_str(context, sizeof(digest), digest, NULL))
	{
		purple_debug_error("jabber", "Failed to get digest for %s cipher.\n",
		    hash_algo);
		g_return_val_if_reached(NULL);
	}
	purple_cipher_context_destroy(context);

	return g_strdup(digest);
}