view mcabber/libjabber/jid.c @ 830:80434fde7cfa

Display presence notification timestamps when they exist These timestamps were used in the roster, but not in the buffer window message nor in the history logfile. Reported by "ze".
author Mikael Berthe <mikael@lilotux.net>
date Wed, 03 May 2006 11:28:41 +0200
parents c3ae9251c197
children
line wrap: on
line source

/*
 *  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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 *  Jabber
 *  Copyright (C) 1998-1999 The Jabber Team http://jabber.org/
 */

/**
 * @file jid.c
 * @brief representation and normalization of JabberIDs
 */

#include "jabber.h"

#ifdef LIBIDN

#  include <stringprep.h>


/**
 * @brief datastructure to build the stringprep caches
 */
typedef struct _jid_prep_entry_st {
    char *preped;	/**< the result of the preparation, NULL if unchanged */
    time_t last_used;	/**< when this result has last been successfully used */
    unsigned int used_count; /**< how often this result has been successfully used */
    int size;		/**< the min buffer size needed to hold the result (strlen+1) */
} *_jid_prep_entry_t;

/**
 * @brief string preparation cache
 */
typedef struct _jid_prep_cache_st {
    xht hashtable;	/**< the hash table containing the preped strings */
    pth_mutex_t mutex;	/**< mutex controling the access to the hashtable */
    const Stringprep_profile *profile;
    			/**< the stringprep profile used for this cache */
} *_jid_prep_cache_t;

/**
 * stringprep cache containging already preped nodes
 *
 * we are using global caches here for two reasons:
 * - I do not see why different instances would want
 *   to have different caches as we are always doing
 *   the same
 * - For per instance caches I would have to modify the
 *   interface of the jid_*() functions which would break
 *   compatibility with transports
 */
_jid_prep_cache_t _jid_prep_cache_node = NULL;

/**
 * stringprep cache containing already preped domains
 */
_jid_prep_cache_t _jid_prep_cache_domain = NULL;

/**
 * stringprep cache containing already preped resources
 */
_jid_prep_cache_t _jid_prep_cache_resource = NULL;

/**
 * walker for cleaning up stringprep caches
 *
 * @param h the hash we are walking through
 * @param key the key of this item
 * @param val the value of this item
 * @param arg delete entries older as this unix timestamp
 */
void _jid_clean_walker(xht h, const char *key, void *val, void *arg) {
    time_t *keep_newer_as = (time_t*)arg;
    _jid_prep_entry_t entry = (_jid_prep_entry_t)val;

    if (entry == NULL)
	return;

    if (entry->last_used <= *keep_newer_as) {
	xhash_zap(h, key);
	if (entry->preped != NULL)
	    free(entry->preped);
	free(entry);

	/* sorry, I have to cast the const away */
	/* any idea how I could delete the key else? */
	if (key != NULL)
	    free((void*)key);
    }
}

/**
 * walk through a single stringprep cache and check which entries have expired
 */
void _jid_clean_single_cache(_jid_prep_cache_t cache, time_t keep_newer_as) {
    /* acquire the lock on the cache */
    pth_mutex_acquire(&(cache->mutex), FALSE, NULL);

    /* walk over all entries */
    xhash_walk(cache->hashtable, _jid_clean_walker, (void*)&keep_newer_as);

    /* we're done, release the lock on the cache */
    pth_mutex_release(&(cache->mutex));
}

/**
 * walk through the stringprep caches and check which entries have expired
 */
void jid_clean_cache() {
    /* XXX make this configurable? */
    time_t keep_newer_as = time(NULL) - 900;

    /* cleanup the nodeprep cache */
    _jid_clean_single_cache(_jid_prep_cache_node, keep_newer_as);
    
    /* cleanup the domain preparation cache */
    _jid_clean_single_cache(_jid_prep_cache_domain, keep_newer_as);
    
    /* cleanup the resourceprep cache */
    _jid_clean_single_cache(_jid_prep_cache_resource, keep_newer_as);
}

/**
 * caching wrapper around a stringprep function
 *
 * @param in_out_buffer buffer containing what has to be stringpreped and that gets the result
 * @param max_len size of the buffer
 * @param cache the used cache, defining also the used stringprep profile
 * @return the return code of the stringprep call
 */
int _jid_cached_stringprep(char *in_out_buffer, int max_len, _jid_prep_cache_t cache) {
    _jid_prep_entry_t preped;
    int result = STRINGPREP_OK;

    /* check that the cache already exists
     * we can not do anything as we don't know which profile has to be used */
    if (cache == NULL) {
	return STRINGPREP_UNKNOWN_PROFILE;
    }

    /* is there something that has to be stringpreped? */
    if (in_out_buffer == NULL) {
	return STRINGPREP_OK;
    }

    /* acquire the lock on the cache */
    pth_mutex_acquire(&(cache->mutex), FALSE, NULL);

    /* check if the requested preparation has already been done */
    preped = (_jid_prep_entry_t)xhash_get(cache->hashtable, in_out_buffer);
    if (preped != NULL) {
	/* we already prepared this argument */
	if (preped->size <= max_len) {
	    /* we can use the result */

	    /* update the statistic */
	    preped->used_count++;
	    preped->last_used = time(NULL);

	    /* do we need to copy the result? */
	    if (preped->preped != NULL) {
		/* copy the result */
		strcpy(in_out_buffer, preped->preped);
	    }

	    result = STRINGPREP_OK;
	} else {
	    /* we need a bigger buffer */
	    result = STRINGPREP_TOO_SMALL_BUFFER;
	}
	
	/* we're done, release the lock on the cache */
	pth_mutex_release(&(cache->mutex));
    } else {
	char *original;

	/* stringprep needs time, release the lock on the cache for the meantime */
	pth_mutex_release(&(cache->mutex));

	/* we have to keep the key */
	original = strdup(in_out_buffer);
	
	/* try to prepare the string */
	result = stringprep(in_out_buffer, max_len, STRINGPREP_NO_UNASSIGNED, cache->profile);

	/* did we manage to prepare the string? */
	if (result == STRINGPREP_OK && original != NULL) {
	    /* generate an entry for the cache */
	    preped = (_jid_prep_entry_t)malloc(sizeof(struct _jid_prep_entry_st));
	    if (preped != NULL) {
		/* has there been modified something? */
		if (j_strcmp(in_out_buffer, original) == 0) {
		    /* no, we don't need to store a copy of the original string */
		    preped->preped = NULL;
		} else {
		    /* yes, store the stringpreped string */
		    preped->preped = strdup(in_out_buffer);
		}
		preped->last_used = time(NULL);
		preped->used_count = 1;
		preped->size = strlen(in_out_buffer)+1;

		/* acquire the lock on the cache again */
		pth_mutex_acquire(&(cache->mutex), FALSE, NULL);

		/* store the entry in the cache */
		xhash_put(cache->hashtable, original, preped);

		/* we're done, release the lock on the cache */
		pth_mutex_release(&(cache->mutex));
	    } else {
		/* we don't need the copy of the key, if there is no memory to store it */
		free(original);
	    }
	} else {
	    /* we don't need the copy of the original value */
	    if (original != NULL)
		free(original);
	}
    }

    return result;
}

/**
 * free a single stringprep cache
 *
 * @param cache the cache to free
 */
void _jid_stop_single_cache(_jid_prep_cache_t *cache) {
    if (*cache == NULL)
	return;

    _jid_clean_single_cache(*cache, time(NULL));
    
    pth_mutex_acquire(&((*cache)->mutex), FALSE, NULL);
    xhash_free((*cache)->hashtable);

    free(*cache);

    *cache = NULL;
}

/**
 * init a single stringprep cache
 *
 * @param cache the cache to init
 * @param prime the prime used to init the hashtable
 * @param profile profile used to prepare the strings
 */
void _jid_init_single_cache(_jid_prep_cache_t *cache, int prime, const Stringprep_profile *profile) {
    /* do not init a cache twice */
    if (*cache == NULL) {
	*cache = (_jid_prep_cache_t)malloc(sizeof(struct _jid_prep_cache_st));
	pth_mutex_init(&((*cache)->mutex));
	(*cache)->hashtable = xhash_new(prime);
	(*cache)->profile = profile;
    }
}

/**
 * free the stringprep caches
 */
void jid_stop_caching() {
    _jid_stop_single_cache(&_jid_prep_cache_node);
    _jid_stop_single_cache(&_jid_prep_cache_domain);
    _jid_stop_single_cache(&_jid_prep_cache_resource);
}

/**
 * init the stringprep caches
 * (do not call this twice at the same time, we do not have the mutexes yet)
 */
void jid_init_cache() {
    /* init the nodeprep cache */
    _jid_init_single_cache(&_jid_prep_cache_node, 2003, stringprep_xmpp_nodeprep);

    /* init the nameprep cache (domains) */
    _jid_init_single_cache(&_jid_prep_cache_domain, 2003, stringprep_nameprep);

    /* init the resourceprep cache */
    _jid_init_single_cache(&_jid_prep_cache_resource, 2003, stringprep_xmpp_resourceprep);
}

/**
 * nameprep the domain identifier in a JID and check if it is valid
 *
 * @param jid data structure holding the JID
 * @return 0 if JID is valid, non zero otherwise
 */
int _jid_safe_domain(jid id) {
    int result=0;

    /* there must be a domain identifier */
    if (j_strlen(id->server) == 0)
	return 1;

    /* nameprep the domain identifier */
    result = _jid_cached_stringprep(id->server, strlen(id->server)+1, _jid_prep_cache_domain);
    if (result == STRINGPREP_TOO_SMALL_BUFFER) {
	/* nameprep wants to expand the string, e.g. conversion from &szlig; to ss */
	size_t biggerbuffersize = 1024;
	char *biggerbuffer = pmalloc(id->p, biggerbuffersize);
	if (biggerbuffer == NULL)
	    return 1;
	strcpy(biggerbuffer, id->server);
	result = _jid_cached_stringprep(biggerbuffer, biggerbuffersize, _jid_prep_cache_domain);
	id->server = biggerbuffer;
    }
    if (result != STRINGPREP_OK)
	return 1;

    /* the namepreped domain must not be longer than 1023 bytes */
    if (j_strlen(id->server) > 1023)
	return 1;

    /* if nothing failed, the domain is valid */
    return 0;
}

/**
 * nodeprep the node identifier in a JID and check if it is valid
 *
 * @param jid data structure holding the JID
 * @return 0 if JID is valid, non zero otherwise
 */
int _jid_safe_node(jid id) {
    int result=0;

    /* it is valid to have no node identifier in the JID */
    if (id->user == NULL)
	return 0;

    /* nodeprep */
    result = _jid_cached_stringprep(id->user, strlen(id->user)+1, _jid_prep_cache_node);
    if (result == STRINGPREP_TOO_SMALL_BUFFER) {
	/* nodeprep wants to expand the string, e.g. conversion from &szlig; to ss */
	size_t biggerbuffersize = 1024;
	char *biggerbuffer = pmalloc(id->p, biggerbuffersize);
	if (biggerbuffer == NULL)
	    return 1;
	strcpy(biggerbuffer, id->user);
	result = _jid_cached_stringprep(biggerbuffer, biggerbuffersize, _jid_prep_cache_node);
	id->user = biggerbuffer;
    }
    if (result != STRINGPREP_OK)
	return 1;

    /* the nodepreped node must not be longer than 1023 bytes */
    if (j_strlen(id->user) > 1023)
	return 1;

    /* if nothing failed, the node is valid */
    return 0;
}

/**
 * resourceprep the resource identifier in a JID and check if it is valid
 *
 * @param jid data structure holding the JID
 * @return 0 if JID is valid, non zero otherwise
 */
int _jid_safe_resource(jid id) {
    int result=0;

    /* it is valid to have no resource identifier in the JID */
    if (id->resource == NULL)
	return 0;

    /* resource prep the resource identifier */
    result = _jid_cached_stringprep(id->resource, strlen(id->resource)+1, _jid_prep_cache_resource);
    if (result == STRINGPREP_TOO_SMALL_BUFFER) {
	/* resourceprep wants to expand the string, e.g. conversion from &szlig; to ss */
	size_t biggerbuffersize = 1024;
	char *biggerbuffer = pmalloc(id->p, biggerbuffersize);
	if (biggerbuffer == NULL)
	    return 1;
	strcpy(biggerbuffer, id->resource);
	result = _jid_cached_stringprep(id->resource, strlen(id->resource)+1, _jid_prep_cache_resource);
	id->resource = biggerbuffer;
    }
    if (result != STRINGPREP_OK)
	return 1;

    /* the resourcepreped node must not be longer than 1023 bytes */
    if (j_strlen(id->resource) > 1023)
	return 1;

    /* if nothing failed, the resource is valid */
    return 0;

}

#else /* no LIBIDN */

/**
 * check if the domain identifier in a JID is valid
 *
 * @param jid data structure holding the JID
 * @return 0 if domain is valid, non zero otherwise
 */
int _jid_safe_domain(jid id) {
    char *str;

    /* there must be a domain identifier */
    if (j_strlen(id->server) == 0)
	return 1;

    /* and it must not be longer than 1023 bytes */
    if (strlen(id->server) > 1023)
	return 1;

    /* lowercase the hostname, make sure it's valid characters */
    for(str = id->server; *str != '\0'; str++)
    {
        *str = tolower(*str);
        if(!(isalnum(*str) || *str == '.' || *str == '-' || *str == '_')) return 1;
    }

    /* otherwise it's okay as far as we can tell without LIBIDN */
    return 0;
}

/**
 * check if the node identifier in a JID is valid
 *
 * @param jid data structure holding the JID
 * @return 0 if node is valid, non zero otherwise
 */
int _jid_safe_node(jid id) {
    char *str;

    /* node identifiers may not be longer than 1023 bytes */
    if (j_strlen(id->user) > 1023)
	return 1;

    /* check for low and invalid ascii characters in the username */
    if(id->user != NULL)
        for(str = id->user; *str != '\0'; str++)
            if(*str <= 32 || *str == ':' || *str == '@' || *str == '<' || *str == '>' || *str == '\'' || *str == '"' || *str == '&') return 1;

    /* otherwise it's okay as far as we can tell without LIBIDN */
    return 0;
}

/**
 * check if the resource identifier in a JID is valid
 *
 * @param jid data structure holding the JID
 * @return 0 if resource is valid, non zero otherwise
 */
int _jid_safe_resource(jid id) {
    /* resources may not be longer than 1023 bytes */
    if (j_strlen(id->resource) > 1023)
	return 1;

    /* otherwise it's okay as far as we can tell without LIBIDN */
    return 0;
}

#endif

/**
 * nodeprep/nameprep/resourceprep the JID and check if it is valid
 *
 * @param jid data structure holding the JID
 * @return NULL if the JID is invalid, pointer to the jid otherwise
 */
jid jid_safe(jid id)
{
    if (_jid_safe_domain(id))
	return NULL;
    if (_jid_safe_node(id))
	return NULL;
    if (_jid_safe_resource(id))
	return NULL;

    return id;
}

jid jid_new(pool p, char *idstr)
{
    char *server, *resource, *type, *str;
    jid id;

    if(p == NULL || idstr == NULL || strlen(idstr) == 0)
        return NULL;

    /* user@server/resource */

    str = pstrdup(p, idstr);

    id = pmalloco(p,sizeof(struct jid_struct));
    id->p = p;

    resource = strstr(str,"/");
    if(resource != NULL)
    {
        *resource = '\0';
        ++resource;
        if(strlen(resource) > 0)
            id->resource = resource;
    }else{
        resource = str + strlen(str); /* point to end */
    }

    type = strstr(str,":");
    if(type != NULL && type < resource)
    {
        *type = '\0';
        ++type;
        str = type; /* ignore the type: prefix */
    }

    server = strstr(str,"@");
    if(server == NULL || server > resource)
    { /* if there's no @, it's just the server address */
        id->server = str;
    }else{
        *server = '\0';
        ++server;
        id->server = server;
        if(strlen(str) > 0)
            id->user = str;
    }

    return jid_safe(id);
}

void jid_set(jid id, char *str, int item)
{
    char *old;

    if(id == NULL)
        return;

    /* invalidate the cached copy */
    id->full = NULL;

    switch(item)
    {
    case JID_RESOURCE:
	old = id->resource;
        if(str != NULL && strlen(str) != 0)
            id->resource = pstrdup(id->p, str);
        else
            id->resource = NULL;
        if(_jid_safe_resource(id))
            id->resource = old; /* revert if invalid */
        break;
    case JID_USER:
        old = id->user;
        if(str != NULL && strlen(str) != 0)
            id->user = pstrdup(id->p, str);
        else
            id->user = NULL;
        if(_jid_safe_node(id))
            id->user = old; /* revert if invalid */
        break;
    case JID_SERVER:
        old = id->server;
        id->server = pstrdup(id->p, str);
        if(_jid_safe_domain(id))
            id->server = old; /* revert if invalid */
        break;
    }

}

char *jid_full(jid id)
{
    spool s;

    if(id == NULL)
        return NULL;

    /* use cached copy */
    if(id->full != NULL)
        return id->full;

    s = spool_new(id->p);

    if(id->user != NULL)
        spooler(s, id->user,"@",s);

    spool_add(s, id->server);

    if(id->resource != NULL)
        spooler(s, "/",id->resource,s);

    id->full = spool_print(s);
    return id->full;
}

/* parses a /resource?name=value&foo=bar into an xmlnode representing <resource name="value" foo="bar"/> */
xmlnode jid_xres(jid id)
{
    char *cur, *qmark, *amp, *eq;
    xmlnode x;

    if(id == NULL || id->resource == NULL) return NULL;

    cur = pstrdup(id->p, id->resource);
    qmark = strstr(cur, "?");
    if(qmark == NULL) return NULL;
    *qmark = '\0';
    qmark++;

    x = _xmlnode_new(id->p, cur, NTYPE_TAG);

    cur = qmark;
    while(cur != '\0')
    {
        eq = strstr(cur, "=");
        if(eq == NULL) break;
        *eq = '\0';
        eq++;

        amp = strstr(eq, "&");
        if(amp != NULL)
        {
            *amp = '\0';
            amp++;
        }

        xmlnode_put_attrib(x,cur,eq);

        if(amp != NULL)
            cur = amp;
        else
            break;
    }

    return x;
}

/* local utils */
int _jid_nullstrcmp(char *a, char *b)
{
    if(a == NULL && b == NULL) return 0;
    if(a == NULL || b == NULL) return -1;
    return strcmp(a,b);
}
int _jid_nullstrcasecmp(char *a, char *b)
{
    if(a == NULL && b == NULL) return 0;
    if(a == NULL || b == NULL) return -1;
    return strcasecmp(a,b);
}

int jid_cmp(jid a, jid b)
{
    if(a == NULL || b == NULL)
        return -1;

    if(_jid_nullstrcmp(a->resource, b->resource) != 0) return -1;
    if(_jid_nullstrcasecmp(a->user, b->user) != 0) return -1;
    if(_jid_nullstrcmp(a->server, b->server) != 0) return -1;

    return 0;
}

/* suggested by Anders Qvist <quest@valdez.netg.se> */
int jid_cmpx(jid a, jid b, int parts)
{
    if(a == NULL || b == NULL)
        return -1;

    if(parts & JID_RESOURCE && _jid_nullstrcmp(a->resource, b->resource) != 0) return -1;
    if(parts & JID_USER && _jid_nullstrcasecmp(a->user, b->user) != 0) return -1;
    if(parts & JID_SERVER && _jid_nullstrcmp(a->server, b->server) != 0) return -1;

    return 0;
}

/* makes a copy of b in a's pool, requires a valid a first! */
jid jid_append(jid a, jid b)
{
    jid next;

    if(a == NULL)
        return NULL;

    if(b == NULL)
        return a;

    next = a;
    while(next != NULL)
    {
        /* check for dups */
        if(jid_cmp(next,b) == 0)
            break;
        if(next->next == NULL)
            next->next = jid_new(a->p,jid_full(b));
        next = next->next;
    }
    return a;
}

xmlnode jid_nodescan(jid id, xmlnode x)
{
    xmlnode cur;
    pool p;
    jid tmp;

    if(id == NULL || xmlnode_get_firstchild(x) == NULL) return NULL;

    p = pool_new();
    for(cur = xmlnode_get_firstchild(x); cur != NULL; cur = xmlnode_get_nextsibling(cur))
    {
        if(xmlnode_get_type(cur) != NTYPE_TAG) continue;

        tmp = jid_new(p,xmlnode_get_attrib(cur,"jid"));
        if(tmp == NULL) continue;

        if(jid_cmp(tmp,id) == 0) break;
    }
    pool_free(p);

    return cur;
}

jid jid_user(jid a)
{
    jid ret;

    if(a == NULL || a->resource == NULL) return a;

    ret = pmalloco(a->p,sizeof(struct jid_struct));
    ret->p = a->p;
    ret->user = a->user;
    ret->server = a->server;

    return ret;
}