view libpurple/protocols/jabber/jutil.c @ 28534:738cd1adb3cf

jabber: Determine if a buddy can receive a file transfer (when we have received caps for all online caps). This will still allow file transfer to clients that don't advertise caps. Disables the file transfer option for gmail buddies Refs #1507
author Marcus Lundblad <ml@update.uu.se>
date Thu, 12 Nov 2009 23:24:43 +0000
parents 4cb882afbd6f
children 464d022d7d6e bc1eb4dacd00
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 */
}

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_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);
}

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;
}

PurpleConversation *
jabber_find_unnormalized_conv(const char *name, PurpleAccount *account)
{
	PurpleConversation *c = NULL;
	GList *cnv;

	g_return_val_if_fail(name != NULL, NULL);

	for(cnv = purple_get_conversations(); cnv; cnv = cnv->next) {
		c = (PurpleConversation*)cnv->data;
		if(purple_conversation_get_type(c) == PURPLE_CONV_TYPE_IM &&
				!purple_utf8_strcasecmp(name, purple_conversation_get_name(c)) &&
				account == purple_conversation_get_account(c))
			return c;
	}

	return NULL;
}

/* The same as purple_util_get_image_checksum, but guaranteed to remain SHA1 */
char *
jabber_calculate_data_sha1sum(gconstpointer data, size_t len)
{
	PurpleCipherContext *context;
	static gchar digest[41];

	context = purple_cipher_context_new_by_name("sha1", NULL);
	if (context == NULL)
	{
		purple_debug_error("jabber", "Could not find sha1 cipher\n");
		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 SHA-1 digest.\n");
		g_return_val_if_reached(NULL);
	}
	purple_cipher_context_destroy(context);

	return g_strdup(digest);
}