/*
 * $Id$
 *
 * resolver related functions
 *
 * Copyright (C) 2006 iptelorg GmbH
 *
 * This file is part of ser, a free SIP server.
 *
 * ser 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
 *
 * For a license to use the ser software under conditions
 * other than those described here, or to purchase support for this
 * software, please contact iptel.org by e-mail at the following addresses:
 *    info@iptel.org
 *
 * ser 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
 */
/* History:
 * --------
 *  2006-07-29  created by andrei
 */


#ifdef USE_DST_BLACKLIST

#include "dst_blacklist.h"
#include "mem/shm_mem.h"
#include "hashes.h"
#include "locking.h"
#include "timer.h"
#include "timer_ticks.h"
#include "ip_addr.h"
#include "error.h"
#include "rpc.h"




struct dst_blst_entry{
	struct dst_blst_entry* next;
	ticks_t expire;
	unsigned short port;
	unsigned char proto;
	unsigned char flags; /* contains the address type + error flags */
	unsigned char ip[4]; /* 4 for ipv4, 16 for ipv6 */ 
};

#define DST_BLST_ENTRY_SIZE(b) \
		(sizeof(struct dst_blst_entry)+((b).flags&BLST_IS_IPV6)*12)


#define DST_BLST_HASH_SIZE		1024
#define DEFAULT_BLST_TIMEOUT		60  /* 1 min. */
#define DEFAULT_BLST_MAX_MEM	250 /* 1 Kb FIXME (debugging)*/
#define DEFAULT_BLST_TIMER_INTERVAL		60 /* 1 min */


static gen_lock_t* blst_lock=0;
static struct timer_ln* blst_timer_h=0;

static volatile unsigned int* blst_mem_used=0;
unsigned int  blst_max_mem=DEFAULT_BLST_MAX_MEM; /* maximum memory used
													for the blacklist entries*/
unsigned int blst_timeout=DEFAULT_BLST_TIMEOUT;
unsigned int blst_timer_interval=DEFAULT_BLST_TIMER_INTERVAL;
struct dst_blst_entry** dst_blst_hash=0;


#define LOCK_BLST()		lock_get(blst_lock)
#define UNLOCK_BLST()	lock_release(blst_lock)


inline static void blst_destroy_entry(struct dst_blst_entry* e)
{
	shm_free(e);
}


static ticks_t blst_timer(ticks_t ticks, struct timer_ln* tl, void* data);


inline static void dst_blst_entry2ip(struct ip_addr* ip,
										struct dst_blst_entry* e)
{
	if (e->flags & BLST_IS_IPV6){
		ip->af=AF_INET6;
		ip->len=16;
	}else{
		ip->af=AF_INET;
		ip->len=4;
	}
	memcpy(ip->u.addr, e->ip, ip->len);
}



inline static unsigned short dst_blst_hash_no(unsigned char proto,
											  struct ip_addr* ip,
											  unsigned short port)
{
	str s1;
	str s2;
	
	s1.s=(char*)ip->u.addr;
	s1.len=ip->len;
	s2.s=(char*)&port;
	s2.len=sizeof(unsigned short);
	return get_hash2_raw(&s1, &s2)%DST_BLST_HASH_SIZE;
}



void destroy_dst_blacklist()
{
	int r;
	struct dst_blst_entry** crt;
	struct dst_blst_entry** tmp;
	struct dst_blst_entry* e;
	
	if (blst_timer_h){
		timer_del(blst_timer_h);
		timer_free(blst_timer_h);
		blst_timer_h=0;
	}
	if (blst_lock){
		lock_destroy(blst_lock);
		lock_dealloc(blst_lock);
		blst_lock=0;
	}
	if (dst_blst_hash){
		for(r=0; r<DST_BLST_HASH_SIZE; r++){
			for (crt=&dst_blst_hash[r], tmp=&(*crt)->next; *crt; 
					crt=tmp, tmp=&(*crt)->next){
			e=*crt;
			*crt=(*crt)->next;
			blst_destroy_entry(e);
			}
		}
		shm_free(dst_blst_hash);
		dst_blst_hash=0;
	}
	if (blst_mem_used){
		shm_free((void*)blst_mem_used);
		blst_mem_used=0;
	}
}



int init_dst_blacklist()
{
	int ret;
	
	ret=-1;
	blst_mem_used=shm_malloc(sizeof(*blst_mem_used));
	if (blst_mem_used==0){
		ret=E_OUT_OF_MEM;
		goto error;
	}
	*blst_mem_used=0;
	dst_blst_hash=shm_malloc(sizeof(struct dst_blst_entry*) *
											DST_BLST_HASH_SIZE);
	if (dst_blst_hash==0){
		ret=E_OUT_OF_MEM;
		goto error;
	}
	memset(dst_blst_hash, 0, sizeof(struct dst_blst_entry*) *
								DST_BLST_HASH_SIZE);
	blst_lock=lock_alloc();
	if (blst_lock==0){
		ret=E_OUT_OF_MEM;
		goto error;
	}
	if (lock_init(blst_lock)==0){
		lock_dealloc(blst_lock);
		blst_lock=0;
		ret=-1;
		goto error;
	}
	blst_timer_h=timer_alloc();
	if (blst_timer_h==0){
		ret=E_OUT_OF_MEM;
		goto error;
	}
	/* fix options */
	blst_max_mem<<=10; /* in Kb */ /* TODO: test with 0 */
	if (blst_timer_interval){
		timer_init(blst_timer_h, blst_timer, 0 ,0); /* slow timer */
		if (timer_add(blst_timer_h, S_TO_TICKS(blst_timer_interval))<0){
			LOG(L_CRIT, "BUG: init_dst_blacklist: failed to add the timer\n");
			timer_free(blst_timer_h);
			blst_timer_h=0;
			goto error;
		}
	}
	return 0;
error:
	destroy_dst_blacklist();
	return ret;
}


/* must be called with the lock held
 * struct dst_blst_entry** head, struct dst_blst_entry* e */
#define dst_blacklist_lst_add(head, e)\
do{ \
	(e)->next=*(head); \
	*(head)=(e); \
}while(0)



/* must be called with the lock held
 * returns a pointer to the blacklist entry if found, 0 otherwise
 * it also deletes expired elements (expire<=now) as it searches
 * proto==PROTO_NONE = wildcard */
inline static struct dst_blst_entry* _dst_blacklist_lst_find(
												struct dst_blst_entry** head,
												struct ip_addr* ip,
												unsigned char proto,
												unsigned short port,
												ticks_t now)
{
	struct dst_blst_entry** crt;
	struct dst_blst_entry** tmp;
	struct dst_blst_entry* e;
	unsigned char type;
	
	type=(ip->af==AF_INET6)*BLST_IS_IPV6;
	for (crt=head, tmp=&(*head)->next; *crt; crt=tmp, tmp=&(*crt)->next){
		e=*crt;
		/* remove old expired entries */
		if ((s_ticks_t)(now-(*crt)->expire)>=0){
			*crt=(*crt)->next;
			*blst_mem_used-=DST_BLST_ENTRY_SIZE(*e);
			blst_destroy_entry(e);
		}else if ((e->port==port) && ((e->flags & BLST_IS_IPV6)==type) &&
				((e->proto==PROTO_NONE) || (proto==PROTO_NONE) ||
					(e->proto==proto)) && 
					(memcmp(ip->u.addr, e->ip, ip->len)==0)){
			return e;
		}
	}
	return 0;
}



/* frees all the expired entries until either there are no more of them
 *  or the total memory used is <= target (to free all of them use -1 for 
 *  targer)
 *  It must be called with LOCK_BLST held (or call dst_blacklist_clean_expired
 *   instead).
 *  params:   target  - free expired entries until no more then taget memory 
 *                      is used  (use 0 to free all of them)
 *            delta   - consider an entry expired if it expires after delta
 *                      ticks from now
 *            timeout - exit after timeout ticks
 *
 *  returns: number of deleted entries
 *  This function should be called periodically from a timer
 */
inline static int _dst_blacklist_clean_expired_unsafe(unsigned int target,
												ticks_t delta,
												ticks_t timeout)
{
	static unsigned short start=0;
	unsigned short h;
	struct dst_blst_entry** crt;
	struct dst_blst_entry** tmp;
	struct dst_blst_entry* e;
	ticks_t start_time;
	ticks_t now;
	int no=0;
	
	now=start_time=get_ticks_raw();
	for(h=start; h!=(start+DST_BLST_HASH_SIZE); h++){
		for (crt=&dst_blst_hash[h%DST_BLST_HASH_SIZE], tmp=&(*crt)->next;
				*crt; crt=tmp, tmp=&(*crt)->next){
			e=*crt;
			if ((s_ticks_t)(now+delta-(*crt)->expire)>=0){
				*crt=(*crt)->next;
				*blst_mem_used-=DST_BLST_ENTRY_SIZE(*e);
				blst_destroy_entry(e);
				no++;
				if (*blst_mem_used<=target)
					goto skip;
			}
		}
		/* check for timeout only "between" hash cells */
		now=get_ticks_raw();
		if ((now-start_time)>=timeout){
			DBG("_dst_blacklist_clean_expired_unsafe: timeout: %d > %d\n",
					TICKS_TO_MS(now-start_time), TICKS_TO_MS(timeout));
			goto skip;
		}
	}
skip:
	start=h; /* next time we start where we left */
	return no;
}



/* frees all the expired entries until either there are no more of them
 *  or the total memory used is <= target (to free all of them use -1 for 
 *  targer)
 *  params:   target  - free expired entries until no more then taget memory 
 *                      is used  (use 0 to free all of them)
 *            delta   - consider an entry expired if it expires after delta
 *                      ticks from now
 *            timeout - exit after timeout ticks
 *
 *  returns: number of deleted entries
 *  This function should be called periodically from a timer
 */
inline static int dst_blacklist_clean_expired(unsigned int target,
												ticks_t delta,
												ticks_t timeout)
{
	int no;
	
	LOCK_BLST();
		no=_dst_blacklist_clean_expired_unsafe(target, delta, timeout);
	UNLOCK_BLST();
	if (no){
		DBG("dst_blacklist_clean_expired, %d entries removed\n", no);
	}
	return no;
}



/* timer */
static ticks_t blst_timer(ticks_t ticks, struct timer_ln* tl, void* data)
{
	dst_blacklist_clean_expired(0, 0, 2); /*spend max. 2 ticks*/
	return (ticks_t)(-1);
}



/* adds a proto ip:port combination to the blacklist
 * returns 0 on success, -1 on error (blacklist full -- would use more then
 *  blst:_max_mem, or out of shm. mem.)
 */
inline static int dst_blacklist_add_ip(unsigned char err_flags, 
									unsigned char proto,
									struct ip_addr* ip, unsigned short port)
{
	int size;
	struct dst_blst_entry* e;
	unsigned short hash;
	ticks_t now;
	int ret;
	
	ret=0;
	if (ip->af==AF_INET){
		err_flags&=~BLST_IS_IPV6; /* make sure the ipv6 flag is reset */
		size=sizeof(struct dst_blst_entry);
	}else{
		err_flags|=BLST_IS_IPV6;
		size=sizeof(struct dst_blst_entry)+12 /* ipv6 addr - 4 */;
	}
	now=get_ticks_raw();
	hash=dst_blst_hash_no(proto, ip, port);
	/* check if the entry already exists */
	LOCK_BLST();
		e=_dst_blacklist_lst_find(&dst_blst_hash[hash], ip, proto, port, now);
		if (e){
			e->flags|=err_flags;
			e->expire=now+S_TO_TICKS(blst_timeout); /* update the timeout */
		}else{
			if ((*blst_mem_used+size)>=blst_max_mem){
				/* first try to free some memory  (~ 12%), but don't
				 * spend more then 250 ms*/
				_dst_blacklist_clean_expired_unsafe(*blst_mem_used/16*14, 0, 
															MS_TO_TICKS(250));
				if (*blst_mem_used+size>=blst_max_mem){
					ret=-1;
					goto error;
				}
			}
			e=shm_malloc(size);
			if (e==0){
				ret=E_OUT_OF_MEM;
				goto error;
			}
			*blst_mem_used+=size;
			e->flags=err_flags;
			e->proto=proto;
			e->port=port;
			memcpy(e->ip, ip->u.addr, ip->len);
			e->expire=now+S_TO_TICKS(blst_timeout); /* update the timeout */
			e->next=0;
			dst_blacklist_lst_add(&dst_blst_hash[hash], e);
		}
error:
	UNLOCK_BLST();
	return ret;
}



/* if no blacklisted returns 0, else returns the blacklist flags */
inline static int dst_is_blacklisted_ip(unsigned char proto,
										struct ip_addr* ip,
										unsigned short port)
{
	struct dst_blst_entry* e;
	unsigned short hash;
	ticks_t now;
	int ret=0;
	
	ret=0;
	now=get_ticks_raw();
	hash=dst_blst_hash_no(proto, ip, port);
	LOCK_BLST();
		e=_dst_blacklist_lst_find(&dst_blst_hash[hash], ip, proto, port, now);
		if (e){
			ret=e->flags;
		}
	UNLOCK_BLST();
	return ret;
}



int dst_blacklist_add(unsigned char err_flags,  struct dest_info* si)
{
	struct ip_addr ip;
	su2ip_addr(&ip, &si->to);
	return dst_blacklist_add_ip(err_flags, si->proto, &ip,
								su_getport(&si->to));
}



int dst_is_blacklisted(struct dest_info* si)
{
	struct ip_addr ip;
	su2ip_addr(&ip, &si->to);
	return dst_is_blacklisted_ip(si->proto, &ip, su_getport(&si->to));
}



/* rpc functions */
void dst_blst_mem_info(rpc_t* rpc, void* ctx)
{
	rpc->add(ctx, "dd",  *blst_mem_used, blst_max_mem);
}



static char* get_proto_name(unsigned char proto)
{
	switch(proto){
		case PROTO_NONE:
			return "*";
		case PROTO_UDP:
			return "udp";
		case PROTO_TCP:
			return "tcp";
		case PROTO_TLS:
			return "tls";
		default:
			return "unknown";
	}
}



/* only for debugging, it helds the lock too long for "production" use */
void dst_blst_debug(rpc_t* rpc, void* ctx)
{
	int h;
	struct dst_blst_entry* e;
	ticks_t now;
	struct ip_addr ip;
	
	now=get_ticks_raw();
	LOCK_BLST();
		for(h=0; h<DST_BLST_HASH_SIZE; h++){
			for(e=dst_blst_hash[h]; e; e=e->next){
				dst_blst_entry2ip(&ip, e);
				rpc->add(ctx, "ssddd", get_proto_name(e->proto), 
										ip_addr2a(&ip), e->port, 
										(s_ticks_t)(now-e->expire)<=0?
										TICKS_TO_S(e->expire-now):
										-TICKS_TO_S(now-e->expire) ,
										e->flags);
			}
		}
	UNLOCK_BLST();
}

#endif /* USE_DST_BLACKLIST */