/**********************************************************************
 * 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: nickserv module

*/

#include "module.h"
#include "path.h"
#include "encrypt.h"
#include "my_sql.h"
#include "dbconf.h"
#define NICKSERV
#include "nickserv.h"
#include "nsmacros.h"
/* lang files */
#include "lang/common.lh"
#include "lang/nickserv.lh"

SVS_Module mod_info = 
  {"nickserv", "7.2", "nickserv core module" };
/* Change Log
  7.2 - #87: +p is not set on private nicks using ns_login
  7.1 -	#62: suspensions replace the forbid setting 
          Added nickserv.9.sql changes
        #61: MaxTimeForAuth setting to expire unauthenticated nicks
  7.0 - #50: nickserv login option to override nick language
        #10 : add nickserv suspensions
        Applied nickserv.8.sql for the above changes
  6.0 - #24 : move e_nick_register event registration to nickserv
        #22 : e_nick_recognize to distinguish +r nick recognitions
        #21 : remove unused/moved fields from nickserv table
        #19 : DefaultLang configuration item is required
        Added nickserv.7.sql for the above changes
  5.1 - 0000340: option NSRegChan to make new users join a channel
        0000330: nickserv links exchange option
        0000327: sadmins SSET nick vhost to set a virtual hostname
        Added nickserv.6.sql changes
  5.0 - 0000305: foreign keys for data integrity 
        Added nickserv.5.sql changes
  4.0 - 0000276: nick password expire option
        0000272: move nickserv security info to a specific table
        0000265: remove nickserv cache system  
        Added nickserv.4.sql changes
  3.0 - Added nickserv.3.sql changes
        0000255: new field to store nick expire time
  2.0 - Added nickserv.2.sql changes
        0000218: NickDefaultOptions to set nick default options
        0000243: protected nick set option
        0000245: use snid to track previous identify  
  1.1 - ghost recognition was not setting +r with DisableNickSecurityCode
*/
          
#define DB_VERSION	9

/** functions and events we require **/
/* void (*FunctionPointer)(void);*/
int mysql_connection;
int e_expire;
int irc;

MOD_REQUIRES
  MOD_FUNC(dbconf_get_or_build)
  MOD_FUNC(mysql_connection)
  MOD_FUNC(irc)
  MOD_FUNC(e_expire) /* we need this to run the expire routines */
MOD_END

/* functions we provide */
ServiceUser* nickserv_suser(void);
int e_nick_identify;
int e_nick_recognize;
int e_nick_register;
int e_nick_delete;
int e_nick_info;
int update_nick_online_info(IRC_User* u, u_int32_t snid, int lang);
int check_nick_security(u_int32_t snid, IRC_User *u, char* pass, char *email, int flags);

MOD_PROVIDES                                                                                
  MOD_FUNC(nickserv_suser)
  MOD_FUNC(update_nick_online_info)
  MOD_FUNC(check_nick_security)
  MOD_FUNC(e_nick_register)
  MOD_FUNC(e_nick_identify)
  MOD_FUNC(e_nick_recognize)
  MOD_FUNC(e_nick_delete)  
  MOD_FUNC(e_nick_info)
MOD_END

/* core events */
void ev_ns_new_user(IRC_User* u, void *null); /* needs to be fixed with IRC_Server */
void ev_ns_quit(IRC_User* u, char* reason);
void ev_ns_kill(IRC_User* u, IRC_User *killer);
void ev_ns_nick_change(IRC_User* u, char* lastnick);
void timer_ns_not_identifed(IRC_User* u, int tag);
int ev_ns_expire(void* dummy1, void* dummy2);
                          
/* commands */
void ns_unknown(IRC_User* s, IRC_User* t);

/* internal functions */
void apply_prefix_nchange(IRC_User *u);
int valid_for_registration(char *nick);
void set_offline_info(IRC_User *u);
int sql_upgrade(int version, int post);
/* int update_nick_online_info(IRC_User* u, u_int32_t snid, int lang); */

/** Local config */
static char* Nick;
static char* Username;
static char* Hostname;
static char* Realname;
static char* LogChan;
static char* NickProtectionPrefix;
static int MaxProtectionNumber;
static int MaxNickChanges;
static int MaxNicksPerEmail;
static int ExpireTime;
static int AgeBonusPeriod;
static int AgeBonusValue;
static int PassExpireTime;
static int NickSecurityCode;
static char* Root;
static int FailedLoginMax;
static int StrongPasswords;
static int SecurityCodeLenght;
static char* DefaultLang;
static int default_lang;
static int MaxTimeForAuth = 0;

DBCONF_PROVIDES
  DBCONF_WORD(Nick,     "NickServ", "Nickserv service nick")
  DBCONF_WORD(Username, "Services", "Nickserv service username")
  DBCONF_WORD(Hostname, "PTlink.net", "Nickserv service hostname")
  DBCONF_STR(Realname,  "Nickserv Service", "Nickserv service real name")
  DBCONF_WORD_OPT(LogChan,  "#Services.log", "Nickserv log channel")
  DBCONF_WORD(NickProtectionPrefix, "PTlink-",
    "Prefix to be applied on forced nick changes (Guest alike)")
  DBCONF_INT(MaxProtectionNumber, "9999",
    "Max. value to accept on random number for forced nick generation")
  DBCONF_INT(MaxNickChanges, "5",
    "Max. forced nick changes before a nick is killed")
  DBCONF_INT(MaxNicksPerEmail, "3",
    "How many nicks can be registered with the same email")
  DBCONF_TIME(ExpireTime, "60d",	
    "How long a nick will be ketp registed whitout logging in")
  DBCONF_TIME(AgeBonusPeriod, "60d",
    "After this time the nick expire time will be increased by AgeBonusValue\n"
    "for each number of AgeBonusPeriod times contained on the nick age")
  DBCONF_TIME(AgeBonusValue, "10d",
    "Bonus increase value for each AgeBonusPeriod contained on nick age")
  DBCONF_TIME(PassExpireTime, "0d", 
    "How long takes for the nick password to expire")
  DBCONF_SWITCH(NickSecurityCode, "on", 
    "Using nick security code will require the users to validate their email\n"
    "using a code received via email. Please check the docs dir for complete\n"
    "documentation on this")
  DBCONF_INT(SecurityCodeLenght, "10", "Security code lengh")
  DBCONF_WORD_OPT(Root, NULL,
    "Root nick, should only be used to add the first nick to the Root group")
  DBCONF_INT(FailedLoginMax, "3",
    "How many failed logins are allowed before killing the user")
  DBCONF_SWITCH(StrongPasswords, "off",
    "Passwords must be at least 8 chars long and contain a non alphabetic char")
  DBCONF_WORD(DefaultLang, "en_us",
    "Default language for unidentifed and new users")
  DBCONF_TIME(MaxTimeForAuth, "15d",
    "How long can a nick be kept without identying ?")
DBCONF_END
              
/* Local variables */
ServiceUser nsu;
int ns_log;

int mod_rehash(void)
{
  if(dbconf_get_or_build(mod_info.name, dbconf_provides) < 0 )
  {
    errlog("Error reading dbconf!");
    return -1;
  }
  lang2index(DefaultLang, default_lang)
  if(default_lang == -1)
  {
    log_log(ns_log, mod_info.name, 
      "Unknownn DefaultLang %s , assuming en_us", DefaultLang);
    default_lang = 0;
  }
  return 0;
}
int mod_load(void)
{

  if(ExpireTime> 0 && ExpireTime < 86400)
    {
      errlog("ExpireTime is too low, minimum is 1 day!");
      return -6;
    }
    
  /* first try to open the log file */
  ns_log = log_open("nickserv","nickserv");
  
  if(ns_log<0)
    {
      errlog("Could not open nickserv log file!");
      return -1;
    }

  if(sql_check_inst_upgrade(mod_info.name, DB_VERSION, sql_upgrade) < 0)
    return -4;
    
  /* Create the nickserv client */
  nsu.u = irc_CreateLocalUser(Nick, Username, Hostname, Hostname,
    Realname,"+ro");
  /* NS should join the log chan */  
  if(LogChan)
    {
      IRC_Chan *chan;
      log_set_irc(ns_log, Nick, LogChan);
      chan = irc_ChanJoin(nsu.u, LogChan, CU_MODE_ADMIN | CU_MODE_OP );
      irc_ChanMode(nsu.u, chan, "+Osnt");
    }
  /* Add unknown command handler */
  irc_AddUMsgEvent(nsu.u, "*", ns_unknown); /* any other msg handler */
  
  /* Add user events and register functions */
  irc_AddEvent(ET_NEW_USER, ev_ns_new_user); /* new user */
  irc_AddEvent(ET_NICK_CHANGE, ev_ns_nick_change); /* nick change */
  irc_AddEvent(ET_QUIT, ev_ns_quit); /* user quit */
  irc_AddEvent(ET_KILL, ev_ns_kill); /* user was killed */

  /* reset nick status */
  sql_query("UPDATE nickserv SET status=0");

  if(ExpireTime == 0)
    stdlog(L_INFO, "ExpireTime is not set, no nicks will expire");      
  else    
    mod_add_event_action(e_expire, (ActionHandler) ev_ns_expire);
  
  return 0;    
}

void
mod_unload(void)
{

  /* remove nickserv and all associated events */
  irc_QuitLocalUser(nsu.u, "Removing service");

  /* remove irc events */
  irc_DelEvent(ET_NEW_USER, ev_ns_new_user);
  irc_DelEvent(ET_NICK_CHANGE, ev_ns_nick_change);
  irc_DelEvent(ET_QUIT, ev_ns_quit);
  irc_DelEvent(ET_KILL, ev_ns_kill);
  lang_delete_assoc(); /* delete lang associations */
}

/* s = service the command was sent to
   u = user the command was sent from */
void ns_unknown(IRC_User* s, IRC_User* t)
{
  send_lang(t, s, UNKNOWN_COMMAND, irc_GetLastMsgCmd());
}


void timer_ns_not_identifed(IRC_User* u, int tag)
{ 
  
  if(MaxNickChanges && u->guest_count >= MaxNickChanges)
  {
    log_log(ns_log, mod_info.name,
      "Killing %s , too many nick changes", irc_UserMask(u));
    irc_Kill(u, nsu.u, "Too many guest nick changes");
    return;
  }
  apply_prefix_nchange(u);
}

/* new user was introduced
  Note: This is also called after a nick change for the new nick
*/
void ev_ns_new_user(IRC_User* u, void *null)
{
  u_int32_t snid = 0;
  u_int32_t flags = 0;
  int lang = 0;
  char *email = NULL;
  char *vhost = NULL;
  
  /* Set language according to the hostname */
  /* u->lang = lang_for_host(u->publichost); */
  u->lang = default_lang;
  lang = u->lang;
  if( sql_singlequery("SELECT snid, flags, lang, email, vhost"
    " FROM nickserv WHERE nick=%s",
    sql_str(irc_lower_nick(u->nick))) )
  {
    int c = 0;
    snid = sql_field_i(c++);
    flags = sql_field_i(c++);
    sql_field_i(c++);
    /* lang = sql_field_i(c++); */
    email = sql_field(c++);    
    vhost = sql_field(c++);
  }
    
  if(snid == 0) /* nick not registered or record not found */
  {
    if(irc_IsUMode(u, UMODE_IDENTIFIED)) /* remove +r just to be safe */
      irc_SvsMode(u, nsu.u, "-r");    
    if(valid_for_registration(u->nick))
      send_lang(u, nsu.u, NICK_IS_NOT_REGISTERED);
    u->flags = 0;
    u->status = 0;
  }
  else if((flags & NFL_SUSPENDED) && 
    sql_singlequery("SELECT reason FROM nickserv_suspensions WHERE snid=%d", snid))
  {
    send_lang(u, nsu.u, NICK_X_IS_SUSPENDED_X, u->nick, sql_field(0));
    apply_prefix_nchange(u);
  }
  else if((u->use_snid == snid) || (u->req_snid == snid)) 
  { /* user already identifed for this nick */
    if(vhost && irccmp(u->publichost, vhost)) /* we need to set the vhost */
      irc_ChgHost(u, vhost);         
    check_nick_security(snid, u, NULL, email, flags);    
    update_nick_online_info(u, snid, lang);
    if(u->req_snid == snid)
      mod_do_event(e_nick_identify, u, &snid);
    else
      mod_do_event(e_nick_recognize, u, &snid);
    u->req_snid = 0;      
  }
  else if(irc_IsUMode(u, UMODE_IDENTIFIED)) /* nick was identified */
  {
    u->flags = flags;  
    update_nick_online_info(u, snid, lang);
    mod_do_event(e_nick_recognize, u, &snid);      
  }
  else if(flags & NFL_PROTECTED)
  {  
    send_lang(u, nsu.u, NICK_IS_PROTECTED);
    apply_prefix_nchange(u);
  }
  else
  {
    u->flags = 0;
    u->status = 0;
    if(irc_IsUMode(u, UMODE_IDENTIFIED)) /* maybe nick was dropped ? */
      irc_SvsMode(u, nsu.u, "-r");
    send_lang(u, nsu.u, NICK_IS_REGISTERED);
    send_lang(u, nsu.u, CHANGE_IN_1M);
    irc_AddUTimerEvent(u, 60 , timer_ns_not_identifed, 0);
  }
}

void ev_ns_nick_change(IRC_User* u, char *lastnick)
{
  irc_CancelUserTimerEvents(u); /* we don't need events for changed nicks */
  set_offline_info(u);
  ev_ns_new_user(u, NULL);    /* Handle as usual nick logon */
}

void ev_ns_quit(IRC_User* u, char* reason)
{
  set_offline_info(u);
}

void ev_ns_kill(IRC_User* u, IRC_User *killer)
{
  set_offline_info(u);
}

/*
 Apply guest nick change (change nick to _nick-number)
 */ 
void apply_prefix_nchange(IRC_User *u)
{
  irc_SvsGuest(u, nsu.u, NickProtectionPrefix, MaxProtectionNumber);
}

/* check if nick can be registered */
int valid_for_registration(char *nick)
{
  static int nlen = 0;
  if(nlen==0)
    nlen = strlen(NickProtectionPrefix);
  if(ircncmp(NickProtectionPrefix, nick, nlen) == 0)
    return 0;
  return -1;
}

void set_offline_info(IRC_User *u)
{
  int i;
  char sql_expire[64];
  
  
  if(AgeBonusPeriod && AgeBonusValue)
    snprintf(sql_expire, sizeof(sql_expire)-1, "%d+FLOOR((%d-t_reg)/%d)*%d",
      (int)irc_CurrentTime+ExpireTime, (int)irc_CurrentTime, AgeBonusPeriod, AgeBonusValue);
  else
    snprintf(sql_expire, sizeof(sql_expire)-1,"%d",
      (int) irc_CurrentTime+ExpireTime);

  if(u->snid && (!MaxTimeForAuth || !NickSecurityCode || IsAuthenticated(u)))
    sql_execute("UPDATE nickserv SET status=0, t_expire=%s, t_seen=%d WHERE snid=%d",
      sql_expire, (int)irc_CurrentTime, u->snid);

  u->snid = 0;
  u->status = 0;
  u->flags = 0;

  for(i = 0; i < 16; ++i)
  {
    array_free(u->extra[i]);
    u->extra[i] = NULL;
  }
}

/* to return the nickserv client */
ServiceUser* nickserv_suser(void)
{
  return &nsu;
}

/* here we are going to use some raw mysql functions
 * because event actions for e_nick_delete may call sql_* functions
 * and override our query state
 */
int ev_ns_expire(void* dummy1, void* dummy2)
{
  time_t expire_start;
  MYSQL_RES* res;
  MYSQL_ROW row;
  int rowc = 0;

  /* expire code goes here */
  res = sql_query("SELECT snid, nick FROM nickserv "
      "WHERE (flags & %d = 0) AND (status & %d = 0) AND "
      "t_expire<%d", 
      NFL_NOEXPIRE | NFL_SUSPENDED, NST_ONLINE, irc_CurrentTime);      

  if(res)
    rowc = mysql_num_rows(res);

  if(rowc)
    log_log(ns_log, mod_info.name, "Will expire %d nick(s)", rowc);
    
  expire_start = time(NULL);    
  /* fetch the data */
  while((row = sql_next_row(res)))
  {
    IRC_User *u;
    u_int32_t snid = atoi(row[0]);
    u = irc_FindUser(row[1]);
    if(u && u->snid) /* nick is online and identified */
    {
      irc_SvsMode(u, nsu.u, "-r");
      u->snid = 0;
    }        
    log_log(ns_log, mod_info.name, "Expiring snid %d, nick %s", snid, row[1]);
    /* call related actions */
    mod_do_event(e_nick_delete, &snid, NULL);
    /* and delete it */
    sql_execute("DELETE FROM nickserv WHERE snid=%d", snid);
  }
  sql_free(res);
  if(rowc)
    log_log(ns_log, mod_info.name, 
      "Expire routine terminated, took %s to expire %d nick(s)", 
      str_time(time(NULL) - expire_start), rowc);
    
  /* expire suspensions */
  res = sql_query("SELECT snid FROM nickserv_suspensions "
    "WHERE duration>0 AND t_when+duration<%d", irc_CurrentTime);
  while((row = sql_next_row(res)))
  {
    u_int32_t snid = atoi(row[0]);
    log_log(ns_log, mod_info.name, "Expiring nick suspension for %d",
      snid);
    sql_execute("DELETE FROM nickserv_suspensions WHERE snid=%d", snid);
    sql_execute("UPDATE nickserv SET flags = (flags & ~%d), t_expire=%d"
      " WHERE snid=%d", NFL_SUSPENDED, irc_CurrentTime+ExpireTime, snid);
  }
  sql_free(res);
  
  return 0;
}

/* this function takes care of sql upgrades */
int sql_upgrade(int version, int post)
{
  if(version == 3 && post)/* Upgrading to version 3, need to set t_expire */
  {
    log_log(ns_log, mod_info.name, "Updating t_expire");    
                    
    if(AgeBonusPeriod && AgeBonusValue)
      sql_execute("UPDATE nickserv "
        "SET t_expire=t_seen+%d+FLOOR((%d-t_reg)/%d)*%d",
        ExpireTime, (int) time(NULL), AgeBonusPeriod, AgeBonusValue);
    else
      sql_execute("UPDATE nickserv "
        "SET t_expire=t_seen+%d,", ExpireTime);
  }
  return 0;
}

int update_nick_online_info(IRC_User* u, u_int32_t snid, int lang)
{
  char sql_expire[64];

  /* update nick info */
  u->snid = snid;
  /* u->lang = lang; */
  u->use_snid = snid;    
  u->status |= NST_ONLINE;
  /* do not update expire time until nick is authenticated */
  if(MaxTimeForAuth && NickSecurityCode && !IsAuthenticated(u))
  {
    send_lang(u, nsu.u, NS_EXPIRE_ON_OLD);
    snprintf(sql_expire, sizeof(sql_expire)-1, "t_expire");
  }
  else
  if(AgeBonusPeriod && AgeBonusValue)
    snprintf(sql_expire, sizeof(sql_expire)-1, "%d+FLOOR((%d-t_reg)/%d)*%d",
      (int)irc_CurrentTime+ExpireTime, (int)irc_CurrentTime, AgeBonusPeriod, AgeBonusValue);
  else
    snprintf(sql_expire, sizeof(sql_expire)-1,"%d",
      (int) irc_CurrentTime+ExpireTime);
        
  return sql_execute("UPDATE nickserv "
   "SET t_ident=%d, t_seen=%d, t_expire=%s,"
   "status = %d WHERE snid=%d",
     irc_CurrentTime, irc_CurrentTime, sql_expire,
     u->status, snid);  
}

/* 
 * Checks a nick security settings
 *  - If the password matches (optional)
 *  - If the email is authenticated
 *  - If the password expired
 *  - If the authentication expired
 * Returns -1 if the password doesn't match
 */
int check_nick_security(u_int32_t snid, IRC_User *u, char* pass, char* email, int flags)
{
  char umodes[10];
  int i = 0;        /* umodes index */
  int diff = 1;
  int full_reg = 0; /* is nick fully registered ? */
  time_t t_lset_pass;
  time_t t_lset_answer;
  time_t t_lauth;
  MYSQL_RES *res;
  MYSQL_ROW row;
  
  res = sql_query("SELECT pass, t_lset_pass, t_lset_answer, t_lauth"
           " FROM nickserv_security WHERE snid=%d", snid);
           
  if(!res || !(row = sql_next_row(res)))
  {
    sql_free(res);
    log_log(ns_log, mod_info.name, "Missing nick security record for %d",
      snid);                  
    return -1;
  }
    
  if(pass)
  {
    if(row[0])
      diff = memcmp(hex_bin(row[0]), encrypted_password(pass), 16);  
    if(diff != 0)
    {
      sql_free(res);
      return -1;
    }
  }

  t_lset_pass = atoi(row[1]);
  t_lset_answer = atoi(row[2]);
  t_lauth = atoi(row[3]);

  if(!NickSecurityCode)
    full_reg = 1;
  else
  {
    if(IsNull(email))
      send_lang(u, nsu.u, MISSING_SET_EMAIL);
    else if(!(flags & NFL_AUTHENTIC))
      send_lang(u, nsu.u, MISSING_AUTH);
    else full_reg = 1;
  }
  
  if(PassExpireTime && (irc_CurrentTime - t_lset_pass > PassExpireTime))
  {
    send_lang(u, nsu.u, NICK_PASSWORD_EXPIRED);    
    full_reg = 0;
  }
  
  if(full_reg) /* nick is fully registered */
  {
    /* set the umodes on nick */
    umodes[i++] = '+';
    if(flags & NFL_PRIVATE)
      umodes[i++] = 'p';
    umodes[i++] = 'r';
    umodes[i] = '\0';
    if(i > 1)
      irc_SvsMode(u, nsu.u, umodes);
    u->status |= NST_FULLREG;
  } else u->status &= ~NST_FULLREG;
  u->flags = flags;
  sql_free(res);
  return 0;
}


syntax highlighted by Code2HTML, v. 0.9.1