/**********************************************************************
 * PTlink IRC Services is (C) CopyRight PTlink IRC Software 1999-2006 *
 *                     http://software.pt-link.net                    *
 * This program is distributed under GNU Public License               *
 * Please read the file COPYING for copyright information.            *
 **********************************************************************

  Description: module sessionlimit

*/

#include "module.h"
#include "dbconf.h"
#include "my_sql.h"
#include "ns_group.h"
#include "nsmacros.h"
#include "lang/sessionlimit.lh"
#include "lang/common.lh"


SVS_Module mod_info =
 /* module, version, description */
{"sessionlimit", "1.2",  "session limit module" };

/* Change Log 
  1.2 - #68: OS EXCEPTION CHANGE for exception owners
        #67: OS EXCEPTION VIEW displaying connections in use
  1.1 - Added missing is_soper() import on the sessionlimit module
        Set +B on users connecting with exception
  1.0 - #8: os_session with better session handling
*/
#define DB_VERSION	1

static int DefaultMaxUsers;
static int MaxHits;
static int GlineTime;

DBCONF_PROVIDES
  DBCONF_INT(DefaultMaxUsers,"3", 
    "Default allowed max. number of connections per host")
  DBCONF_INT(MaxHits, "5",
    "How many times times users from a host will be killed before getting glined")
  DBCONF_TIME(GlineTime, "5h", 
    "Time to be used on the session glined when MaxHits is reached")
DBCONF_END
 
/** functions/events we require **/
ServiceUser* (*operserv_suser)(void);
static int e_expire = 0;

MOD_REQUIRES
  DBCONF_FUNCTIONS
  MOD_FUNC(operserv_suser)
  MOD_FUNC(is_soper)
  MOD_FUNC(is_sadmin)
  MOD_FUNC(e_expire) /* we need this to run the expire routines */
MOD_END

/* host record structure */
typedef struct HostRecord_s HostRecord;
struct HostRecord_s
{
  char host[HOSTLEN];
  int count;
  int limit;
  int limit_hits;
  HostRecord *hnext;
};

/* session hosts hash size */
#define HOST_HSIZE 16384

/* internal data */
static HostRecord *sessionTable[HOST_HSIZE];
static DBMem* dbm_exceptions;

/* internal functions */
static void os_session(IRC_User *s, IRC_User *u);
static void os_exception(IRC_User *s, IRC_User *u);
static int ev_sessionlimit_new_user(IRC_User* u, void *s);
static int ev_sessionlimit_quit(IRC_User* u, char *lastquit);
static int ev_sessionlimit_kill(IRC_User* u, IRC_User *killer);
static HostRecord* add_to_session(char *host);
static void del_from_session(char *host);
static HostRecord* find_session(char *host);
static unsigned int hash_host(const char* host);
static int ev_exceptions_expire(void* dummy1, void* dummy2);

/* Local variables */
static ServiceUser *osu;
static int os_log;

/* Local functions  - commands */
static void os_session_list(IRC_User *s, IRC_User *u);
static void os_session_view(IRC_User *s, IRC_User *u);
static void os_exception_add(IRC_User *s, IRC_User *u);
static void os_exception_del(IRC_User *s, IRC_User *u);
static void os_exception_list(IRC_User *s, IRC_User *u);
static void os_exception_change(IRC_User *s, IRC_User *u);

/* Local functions - utility */
static int import_bot_hostrules(void);

/** rehash code **/
int mod_rehash(void)
{
  if(dbconf_get_or_build(mod_info.name, dbconf_provides) < 0 )
  {
    errlog("Error reading dbconf!");
    return -1;
  }
  return 0;
}

/** load code **/
int mod_load(void)
{
  int r;
  /* initialize dbmem for exceptions */
  dbm_exceptions = dbmem_init("session_exceptions", 128);
  r = sql_check_inst_upgrade(mod_info.name, DB_VERSION, NULL);
  
  if(r < 0)
    return -1;

  /* try to load existing exceptions */
  if(dbmem_load(dbm_exceptions) < 0 )
    return -3;
    
  /* module is being installed, see if we have hostrules to import */
  if((r == 1) && sql_find_module("os_hostrule"))
  {
    if(import_bot_hostrules() < 0)
      return -2;
  }

  os_log = log_handle("operserv");
  osu = operserv_suser();  
  
  suser_add_cmd(osu, "SESSION", os_session, 
    OS_SESSION_SUMMARY, OS_SESSION_HELP);
    
  suser_add_cmd(osu, "EXCEPTION", os_exception, 
    OS_EXCEPTION_SUMMARY, OS_EXCEPTION_HELP);    
    
  /* Add user events */
  irc_AddEvent(ET_NEW_USER, ev_sessionlimit_new_user); /* new user */
  irc_AddEvent(ET_QUIT, ev_sessionlimit_quit); /* user quit */
  irc_AddEvent(ET_KILL, ev_sessionlimit_kill); /* user kill */
  
  mod_add_event_action(e_expire, (ActionHandler) ev_exceptions_expire);
  return 0;
}

void mod_unload(void)
{
  irc_DelEvent(ET_NEW_USER, ev_sessionlimit_new_user);
  irc_DelEvent(ET_QUIT, ev_sessionlimit_quit);
  irc_DelEvent(ET_KILL, ev_sessionlimit_kill);
  mod_del_event_action(e_expire, (ActionHandler) ev_exceptions_expire);
  dbmem_free(dbm_exceptions);
}
        
void os_session(IRC_User *s, IRC_User *u)
{
  char* cmd;
  u_int32_t source_snid;

  CHECK_IF_IDENTIFIED_NICK

  if (!is_soper(u->snid))
  {
    send_lang(u, s, PERMISSION_DENIED);
    return;
  }

  cmd = strtok(NULL, " ");
  /* check syntax */
  if(!cmd)
    send_lang(u, s, OS_SESSION_SYNTAX);
  else
  /* list command */
  if(!strcasecmp(cmd, "LIST"))
    os_session_list(s, u);
  else
  if(!strcasecmp(cmd, "VIEW"))
    os_session_view(s, u);
  else
    send_lang(u, s, OS_SESSION_SYNTAX);
    
}

/* list sessions with connections above a given number */
void os_session_list(IRC_User *s, IRC_User *u)
{
  char *nstr = strtok(NULL, " ");
  if(!nstr || !isdigit(*nstr))
    send_lang(u, s, OS_SESSION_LIST_SYNTAX);
  else
  {
    int hashv;
    HostRecord *hr;
    int num = atoi(nstr);
    send_lang(u, s, OS_SESSION_LIST_HEADER_X, num);
    for(hashv = 0; hashv < HOST_HSIZE; ++hashv)
    {
      hr = sessionTable[hashv]; 
      while(hr)
      {
        if(hr->count > num)
        {
          if(hr->limit_hits)
            send_lang(u, s, OS_SESSION_LIST_ITEM_X_X_X_X, 
              hr->host, hr->count, hr->limit, hr->limit_hits);
          else
            send_lang(u, s, OS_SESSION_LIST_ITEM_X_X_X, 
              hr->host, hr->count, hr->limit);
        }
        hr = hr ->hnext;
      }
    }
    send_lang(u, s, OS_SESSION_LIST_TAIL);
  }
}

void os_session_view(IRC_User *s, IRC_User *u)
{
  char *host = strtok(NULL, " ");
  HostRecord *hr;
  if(host == NULL)
    send_lang(u, s, OS_SESSION_VIEW_SYNTAX);
  else
  if((hr = find_session(host)) == NULL)
    send_lang(u, s, OS_SESSION_VIEW_X_NOT_FOUND, host);
  else
  {
    IRC_UserList gl;
    IRC_User* list_u = irc_GetGlobalList(&gl);
    send_lang(u, s, OS_SESSION_VIEW_HEADER_X, hr->host);
    while(list_u)
    {
      if(strcmp(hr->host, list_u->realhost) == 0) /* we have a match*/
      	send_lang(u, s, OS_SESSION_VIEW_ITEM_X_X_X,
      	  list_u->nick, irc_UserMaskP(list_u), list_u->info);
      list_u = irc_GetNextUser(&gl);
    }
    send_lang(u, s, OS_SESSION_VIEW_TAIL);
  }
}



/* handle a new user event */
int ev_sessionlimit_new_user(IRC_User* u, void *s)
{
  HostRecord *hrec;

  /* check for SessionLimit */  
  hrec = add_to_session(u->realhost);
  if(hrec->limit > DefaultMaxUsers)
    irc_SvsMode(u, osu->u, "+B");
  /* we reached session limit for this host */
  if(hrec->limit && (hrec->count > hrec->limit)) 
  {
    if(!MaxHits || ((++hrec->limit_hits) < MaxHits))
    {
      log_log(os_log, mod_info.name, 
        "Killing %s , exceeded connections limit %d/%d",
      irc_UserMask(u), hrec->count, hrec->limit);
      irc_Kill(u, osu->u, 
        "Exceeded maximum number of connections for this host");  
      /* user was deleted, we need to abort the event*/        
      irc_AbortThisEvent(); 
    }
    else
    {
      char gmask[HOSTLEN+3];        
      snprintf(gmask, HOSTLEN+3, "*@%s", u->realhost);                  
      log_log(os_log, mod_info.name, 
        "Glining  %s, exceeded connections limit %d/%d",
        gmask, hrec->count, hrec->limit);            
      irc_Gline(osu->u, osu->u->nick, gmask, GlineTime,
        "Exceeded maximum number of connections for this host");
    }
  }
  return 0;
}

int ev_sessionlimit_quit(IRC_User* u, char *lastquit)
{
  del_from_session(u->realhost);
  return 0;
}

int ev_sessionlimit_kill(IRC_User* u, IRC_User *killer)
{
  del_from_session(u->realhost);
  return 0;
}  

/* returns a session in memory */
HostRecord* find_session(char *host)
{
  HostRecord* hr;
  u_int32_t hashv;

  hashv = hash_host(host);
  hr = sessionTable[hashv];
  while(hr && strcasecmp(hr->host, host) != 0)
      hr = hr->hnext;

  return hr;
}
                  
/* adds a session to the hash table */
HostRecord* add_to_session(char *host)
{
  HostRecord* hr;
  u_int32_t hashv;

  hr = find_session(host); /* check if its already in mem */
  if(IsNull(hr))
    {
      char **row;
      row = dbmem_find_exact(dbm_exceptions, host, 0);

      hr = malloc(sizeof(HostRecord));
      bzero(hr,sizeof(HostRecord));    
      if(row)
        hr->limit = atoi(row[5]);
      else
        hr->limit = DefaultMaxUsers;
      hashv = hash_host(host);
      hr->hnext = sessionTable[hashv];
      strncpy(hr->host, host, HOSTLEN);
      sessionTable[hashv] = hr;
    }
  hr->count++;
  return hr;
}

void del_from_session(char *host)
{
  HostRecord *hr, *prev;
  int hashv;
  
  hashv = hash_host(host);
  hr = sessionTable[hashv];
  prev = NULL;
  
  if(IsNull(hr))
    return;
    
  while(hr && strcasecmp(hr->host, host) != 0)
    {
      prev = hr;    
      hr = hr->hnext;
    }
        
  if(IsNull(hr))
    return; /* not found */
    
  if(--(hr->count)==0) /* zero count, remove record from mem */
    {
      if(prev)
        prev->hnext= hr->hnext;
      else
        sessionTable[hashv] = hr->hnext;
      free(hr);
    }
}

unsigned int hash_host(const char* host)
{
  unsigned int h = 0;
                                                                                
  while (*host)
    {
      h = (h << 4) - (h + (unsigned char)ToLower(*host++));
    }
                                                                                
  return(h & (HOST_HSIZE - 1));
}

void os_exception(IRC_User *s, IRC_User *u)
{

  char *cmd = strtok(NULL, " ");
  
  /* check syntax */
  if(!cmd)
    send_lang(u, s, OS_EXCEPTION_SYNTAX);
  else
  /* list command */
  if(!strcasecmp(cmd, "ADD"))
    os_exception_add(s, u);
  else
  if(!strcasecmp(cmd, "DEL"))
    os_exception_del(s, u);
  else  
  if(!strcasecmp(cmd, "LIST"))
    os_exception_list(s, u);
  else
  if(!strcasecmp(cmd, "CHANGE"))
    os_exception_change(s, u);  
  else
    send_lang(u, s, OS_EXCEPTION_SYNTAX);
    
}

static void os_exception_add(IRC_User *s, IRC_User *u)
{
  u_int32_t owner_snid;
  char **row;
  char *owner = strtok(NULL, " ");
  char *ex_time = strtok(NULL, " ");
  char *mask = strtok(NULL, " ");
  char *limit = strtok(NULL, " ");
  char *reason = strtok(NULL, "");  

  /* check for required privilege */
  if(!is_sadmin(u->snid))
    send_lang(u, s, PERMISSION_DENIED);
  else  
  if(!owner || !ex_time | !mask | !limit || !reason || (limit && atoi(limit)<1))
    send_lang(u, s, OS_EXCEPTION_SYNTAX);
  else
  /* check if hostname is valid */
  if(!irc_IsValidHostname(mask))
    send_lang(u, s, OS_EXCEPTION_INVALID_HOST_X, mask);
  else
  /* check if time is valid */
  if(ftime_str(ex_time) == -1)
    send_lang(u, s, INVALID_TIME_X, ex_time);
  else
  /* check if the owner exists */
  if((owner_snid = nick2snid(owner)) == 0)
    send_lang(u, s, NICK_X_NOT_REGISTERED, owner);
  else
  /* check if exception already exists */
  if((row = dbmem_find_exact(dbm_exceptions, mask, 0)))
    send_lang(u, s, OS_EXCEPTION_ADD_ALREADY_EXISTS_X, mask);
  else
  {
    char* d_row[7];
    int i = 0;
    d_row[i++] = mask; /* hostmask */
    d_row[i++] = itoa(u->snid); /* who_snid */
    d_row[i++] = itoa(owner_snid); /* owner_snid */
    d_row[i++] = itoa(irc_CurrentTime); /* t_when */
    d_row[i++] = itoa(ftime_str(ex_time)); 	 /* duration */
    d_row[i++] = itoa(atoi(limit)); /* limit */
    d_row[i++] = reason; /* reason */
    if(dbmem_insert(dbm_exceptions, d_row)<0)
      send_lang(u, s, UPDATE_FAIL);
    else
    {
      HostRecord *hr = find_session(mask);
      send_lang(u, s, OS_EXCEPTION_ADD_OK_X, mask);
      if(hr)
        hr->limit = atoi(limit);
    }
  }
}

static void os_exception_del(IRC_User *s, IRC_User *u)
{

  char *mask = strtok(NULL, " ");

  /* check for required privilege */
  if(!is_sadmin(u->snid))
    send_lang(u, s, PERMISSION_DENIED);
  else  
  if(!mask)
    send_lang(u, s, OS_EXCEPTION_SYNTAX);
  else
  /* check if exception already exists */
  if(dbmem_find_exact(dbm_exceptions, mask, 0) == NULL)
    send_lang(u, s, OS_EXCEPTION_NOT_FOUND_X, mask);
  else
  if(dbmem_delete_current(dbm_exceptions) < 0)
      send_lang(u, s, UPDATE_FAIL);
  else
  {
    HostRecord *hr = find_session(mask);
    send_lang(u, s, OS_EXCEPTION_DELETED_X, mask);
    if(hr)
      hr->limit = DefaultMaxUsers;
  }      
}

static void os_exception_list(IRC_User *s, IRC_User *u)
{
  char **row;
  int privilege = is_sadmin(u->snid);
  /* we run the expire before the list, to make a clean list */
  ev_exceptions_expire(NULL, NULL);
  send_lang(u, s, OS_EXCEPTION_LIST_HEADER);
  row = dbmem_first_row(dbm_exceptions);
  while(row)
  {
    if(privilege || (u->snid == atoi(row[2])))
    {
      HostRecord* hr = find_session(row[0]);
      if(hr)
        send_lang(u, s, OS_EXCEPTION_LIST_X_X_X_X,
    	  row[0], row[5], hr->count, row[6]);
      else
        send_lang(u, s, OS_EXCEPTION_LIST_X_X_X,
    	  row[0], row[5], row[6]);
    }
    row = dbmem_next_row(dbm_exceptions);
  };
  send_lang(u, s, OS_EXCEPTION_LIST_TAIL);
}

static void os_exception_change(IRC_User *s, IRC_User *u)
{

  int privilege = is_sadmin(u->snid);
  char *old_host = strtok(NULL, " ");
  char *new_host = strtok(NULL, " ");

  /* check for required privilege */
  if(!old_host || !new_host)
    send_lang(u, s, OS_EXCEPTION_CHANGE_SYNTAX);
  else 
  /* check if hostname is valid */
  if(!irc_IsValidHostname(new_host))
    send_lang(u, s, OS_EXCEPTION_INVALID_HOST_X, new_host);
  else
  /* check if exception already exists */
  if(dbmem_find_exact(dbm_exceptions, new_host, 0))
    send_lang(u, s, OS_EXCEPTION_ALREADY_X_EXISTS, new_host);
  else
  if(dbmem_find_exact(dbm_exceptions, old_host, 0) == NULL)
    send_lang(u, s, OS_EXCEPTION_NOT_FOUND_X, old_host);
  else
  {
    char** row = dbmem_current_row(dbm_exceptions);
    if(!privilege && (u->snid != atoi(row[2])))
      send_lang(u, s, OS_EXCEPTION_CHANGE_DENIED, old_host);
    else
    {
      if(dbmem_replace_key(dbm_exceptions, new_host) > 0)
      {
        log_log(os_log, mod_info.name, "%s CHANGED exception host from %s to %s",
          u->nick, old_host, new_host); 
        HostRecord *old_hr = find_session(old_host);
        HostRecord *new_hr = find_session(new_host);

        if(old_hr)
          old_hr->limit = DefaultMaxUsers;
        if(new_hr)
          new_hr->limit = atoi(row[5]);
        if(dbmem_find_exact(dbm_exceptions, old_host, 0))
          dbmem_delete_current(dbm_exceptions);
        send_lang(u, s, OS_EXCEPTION_CHANGED);          
        irc_SendSanotice(s, "%s CHANGED exception host from %s to %s",
          u->nick, old_host, new_host);
      }
      else
        send_lang(u, s, UPDATE_FAIL);
    }
  }
}
/* import_bot_hostrules - import bot type hostules from hostrule table 
 *  returns:
 *	Number of imported rows, <0 for error
 */
static int import_bot_hostrules(void)
{
  MYSQL_RES *res;
  MYSQL_ROW row;
  char* d_row[7];
  int count = 0;
  res = sql_query("SELECT host, UNIX_TIMESTAMP(t_create), param, message FROM os_hostrule "
  	"WHERE rtype=512");
  if(!res)
    return -1;
  while((row = sql_next_row(res)))
  {
    int i = 0;
    d_row[i++] = row[0]; /* hostmask */
    d_row[i++] = NULL; /* who_snid */
    d_row[i++] = NULL; /* owner_snid */
    d_row[i++] = row[1]; /* t_when */
    d_row[i++] = "0"; 	 /* duration */
    d_row[i++] = row[2]; /* limit */
    d_row[i++] = row[3]; /* reason */
    if(dbmem_insert(dbm_exceptions, d_row) <0 )
    {
      sql_free(res);
      return -2;
    }
    ++count;
 }
 sql_free(res); 
 /* delete imported hostrules */
 sql_execute("DELETE FROM os_hostrule WHERE rtype=512");
  
 return count;
}

/* ev_exceptions_expire - deleted expired exceptions
 *  returns:
 *	Number of imported rows, <0 for error
 */
int ev_exceptions_expire(void* dummy1, void* dummy2)
{
  dbmem_expire(dbm_exceptions, 3, 4);
  return 0;
}


syntax highlighted by Code2HTML, v. 0.9.1