/* Session limiting module. * Based on code copyright (c) 1999-2000 Andrew Kempe (TheShadow) * E-mail: * * IRC Services is copyright (c) 1996-2007 Andrew Church. * E-mail: * Parts written by Andrew Kempe and others. * This program is free but copyrighted software; see the file COPYING for * details. */ #include "services.h" #include "modules.h" #include "conffile.h" #include "commands.h" #include "language.h" #include "operserv.h" #include "maskdata.h" #include "akill.h" #include "sessions.h" /*************************************************************************/ /* SESSION LIMITING * * The basic idea of session limiting is to prevent one host from having more * than a specified number of sessions (client connections/clones) on the * network at any one time. To do this we have a list of sessions and * exceptions. Each session structure records information about a single host, * including how many clients (sessions) that host has on the network. When a * host reaches it's session limit, no more clients from that host will be * allowed to connect. * * When a client connects to the network, we check to see if their host has * reached the default session limit per host, and thus whether it is allowed * any more. If it has reached the limit, we kill the connecting client; all * the other clients are left alone. Otherwise we simply increment the counter * within the session structure. When a client disconnects, we decrement the * counter. When the counter reaches 0, we free the session. * * Exceptions allow one to specify custom session limits for a specific host * or a range thereof. The first exception that the host matches is the one * used. * * "Session Limiting" is likely to slow down services when there are frequent * client connects and disconnects. The size of the exception list can also * play a large role in this performance decrease. It is therefore recommened * that you keep the number of exceptions to a minimum. A very simple hashing * method is currently used to store the list of sessions. I'm sure there is * room for improvement and optimisation of this, along with the storage of * exceptions. Comments and suggestions are more than welcome! * * -TheShadow (02 April 1999) */ /*************************************************************************/ static Module *module; static Module *module_operserv; static Module *module_akill; /* create_kill() imported from operserv/akill */ static void (*p_create_akill)(char *mask, const char *reason, const char *who, time_t expiry); static int db_opened = 0; static char * ExceptionDBName; static int WallOSException; static int WallExceptionExpire; static int32 DefSessionLimit; static time_t ExceptionExpiry; static int32 MaxSessionLimit; static char * SessionLimitExceeded; static char * SessionLimitDetailsLoc; static int SessionLimitAutokill; static time_t SessionLimitMinKillTime; static int32 SessionLimitMaxKillCount; static time_t SessionLimitAutokillExpiry; static char * SessionLimitAutokillReason; /*************************************************************************/ typedef struct session_ Session; struct session_ { Session *prev, *next; char *host; int count; /* Number of clients with this host */ int killcount; /* Number of kills for this session */ time_t lastkill; /* Time of last kill */ }; #define HASH_STATIC static #include "hash.h" #define add_session _add_session #define del_session _del_session DEFINE_HASH(session, Session, host) #undef del_session #undef add_session static void do_session(User *u); static void do_exception(User *u); static Command cmds[] = { {"SESSION", do_session, is_services_oper, OPER_HELP_SESSION, -1,-1}, {"EXCEPTION", do_exception, is_services_oper, OPER_HELP_EXCEPTION, -1,-1}, {NULL} }; /*************************************************************************/ /************************* Session List Display **************************/ /*************************************************************************/ /* Syntax: SESSION LIST threshold * Lists all sessions with atleast threshold clients. * The threshold value must be greater than 1. This is to prevent * accidental listing of the large number of single client sessions. * * Syntax: SESSION VIEW host * Displays detailed session information about the supplied host. */ static void do_session(User *u) { Session *session; MaskData *exception; const char *cmd = strtok(NULL, " "); const char *param1 = strtok(NULL, " "); int mincount; if (!cmd) cmd = ""; if (stricmp(cmd, "LIST") == 0) { if (!param1) { syntax_error(s_OperServ, u, "SESSION", OPER_SESSION_LIST_SYNTAX); } else if ((mincount = atoi(param1)) <= 1) { notice_lang(s_OperServ, u, OPER_SESSION_INVALID_THRESHOLD); } else { notice_lang(s_OperServ, u, OPER_SESSION_LIST_HEADER, mincount); notice_lang(s_OperServ, u, OPER_SESSION_LIST_COLHEAD); for (session = first_session(); session; session = next_session()){ if (session->count >= mincount) notice_lang(s_OperServ, u, OPER_SESSION_LIST_FORMAT, session->count, session->host); } } } else if (stricmp(cmd, "VIEW") == 0) { if (!param1) { syntax_error(s_OperServ, u, "SESSION", OPER_SESSION_VIEW_SYNTAX); } else { session = get_session(param1); if (!session) { notice_lang(s_OperServ, u, OPER_SESSION_NOT_FOUND, param1); } else { exception = get_matching_maskdata(MD_EXCEPTION, param1); notice_lang(s_OperServ, u, OPER_SESSION_VIEW_FORMAT, param1, session->count, exception ? exception->limit : DefSessionLimit); } } } else { syntax_error(s_OperServ, u, "SESSION", OPER_SESSION_SYNTAX); } } /*************************************************************************/ /********************* Internal Session Functions ************************/ /*************************************************************************/ /* Free a session structure. Separate from del_session() because the * module cleanup code also uses it. */ static inline void free_session(Session *session) { free(session->host); free(session); } /*************************************************************************/ /* Attempt to add a host to the session list. If the addition of the new host * causes the the session limit to be exceeded, kill the connecting user. * Returns 1 if the host was added or 0 if the user was killed. */ static int add_session(const char *nick, const char *host) { Session *session; MaskData *exception; int sessionlimit = 0; char buf[BUFSIZE]; time_t now = time(NULL); session = get_session(host); if (session) { exception = get_matching_maskdata(MD_EXCEPTION, host); sessionlimit = exception ? exception->limit : DefSessionLimit; if (sessionlimit != 0 && session->count >= sessionlimit) { if (SessionLimitExceeded) notice(s_OperServ, nick, SessionLimitExceeded, host); if (SessionLimitDetailsLoc) notice(s_OperServ, nick, SessionLimitDetailsLoc); if (SessionLimitAutokill && module_akill) { if (now <= session->lastkill + SessionLimitMinKillTime) { session->killcount++; if (session->killcount >= SessionLimitMaxKillCount) { snprintf(buf, sizeof(buf), "*@%s", host); p_create_akill(buf,SessionLimitAutokillReason, s_OperServ, now + SessionLimitAutokillExpiry); session->killcount = 0; } } else { session->killcount = 1; } session->lastkill = now; } /* We don't use kill_user() because a user stucture has not yet * been created. Simply kill the user. -TheShadow */ send_cmd(s_OperServ, "KILL %s :%s (Session limit exceeded)", nick, s_OperServ); return 0; } else { session->count++; return 1; } /* not reached */ } /* Session does not exist, so create it */ session = scalloc(sizeof(Session), 1); session->host = sstrdup(host); session->count = 1; session->killcount = 0; session->lastkill = 0; _add_session(session); return 1; } /*************************************************************************/ static void del_session(const char *host) { Session *session; if (debug >= 2) module_log("debug: del_session() called"); session = get_session(host); if (!session) { wallops(s_OperServ, "WARNING: Tried to delete non-existent session: \2%s", host); module_log("Tried to delete non-existent session: %s", host); return; } if (session->count > 1) { session->count--; return; } _del_session(session); if (debug >= 2) module_log("debug: del_session(): free session structure"); free_session(session); if (debug >= 2) module_log("debug: del_session() done"); } /*************************************************************************/ /************************ Exception Manipulation *************************/ /*************************************************************************/ /* Syntax: EXCEPTION ADD [+expiry] mask limit reason * Adds mask to the exception list with limit as the maximum session * limit and +expiry as an optional expiry time. * * Syntax: EXCEPTION DEL mask * Deletes the first exception that matches mask exactly. * * Syntax: EXCEPTION LIST [mask] * Lists all exceptions or those matching mask. * * Syntax: EXCEPTION VIEW [mask] * Displays detailed information about each exception or those matching * mask. * * Syntax: EXCEPTION MOVE num newnum * Moves the exception with number num to have number newnum. */ static void do_exception_add(User *u); static void do_exception_del(User *u); static void do_exception_list(User *u, int is_view); static void do_exception_move(User *u); static int exception_del_callback(User *u, int num, va_list args); static int exception_list(User *u, MaskData *except, int *sent_header, int is_view); static int exception_list_callback(User *u, int num, va_list args); static void do_exception(User *u) { const char *cmd = strtok(NULL, " "); if (!cmd) cmd = ""; if (stricmp(cmd, "ADD") == 0) { do_exception_add(u); } else if (stricmp(cmd, "DEL") == 0) { do_exception_del(u); } else if (stricmp(cmd, "MOVE") == 0) { do_exception_move(u); } else if (stricmp(cmd, "LIST") == 0 || stricmp(cmd, "VIEW") == 0) { do_exception_list(u, stricmp(cmd,"VIEW")==0); } else { syntax_error(s_OperServ, u, "EXCEPTION", OPER_EXCEPTION_SYNTAX); } } /*************************************************************************/ static void do_exception_add(User *u) { char *mask, *reason, *expiry, *limitstr; time_t expires; int limit, i; MaskData *except; time_t now = time(NULL); if (maskdata_count(MD_EXCEPTION) >= MAX_MASKDATA) { notice_lang(s_OperServ, u, OPER_EXCEPTION_TOO_MANY); return; } mask = strtok(NULL, " "); if (mask && *mask == '+') { expiry = mask+1; mask = strtok(NULL, " "); } else { expiry = NULL; } limitstr = strtok(NULL, " "); reason = strtok_remaining(); if (!reason) { syntax_error(s_OperServ, u, "EXCEPTION", OPER_EXCEPTION_ADD_SYNTAX); return; } expires = expiry ? dotime(expiry) : ExceptionExpiry; if (expires < 0) { notice_lang(s_OperServ, u, BAD_EXPIRY_TIME); return; } else if (expires > 0) { expires += now; } limit = (limitstr && isdigit(*limitstr)) ? atoi(limitstr) : -1; if (limit < 0 || limit > MaxSessionLimit) { notice_lang(s_OperServ, u, OPER_EXCEPTION_INVALID_LIMIT, MaxSessionLimit); return; } else if (strchr(mask, '!') || strchr(mask, '@')) { notice_lang(s_OperServ, u, OPER_EXCEPTION_INVALID_HOSTMASK); return; } else if (get_maskdata(MD_EXCEPTION, strlower(mask))) { notice_lang(s_OperServ, u, OPER_EXCEPTION_ALREADY_PRESENT, mask, limit); } else { i = 0; for (except = first_maskdata(MD_EXCEPTION); except; except = next_maskdata(MD_EXCEPTION)) i = except->num; except = scalloc(1, sizeof(*except)); except->mask = sstrdup(mask); except->limit = limit; except->reason = sstrdup(reason); except->time = now; strscpy(except->who, u->nick, NICKMAX); except->expires = expires; except->num = i+1; add_maskdata(MD_EXCEPTION, except); if (WallOSException) { char buf[BUFSIZE]; expires_in_lang(buf, sizeof(buf), NULL, expires); wallops(s_OperServ, "%s added a session limit exception of" " \2%d\2 for \2%s\2 (%s)", u->nick, limit, mask, buf); } notice_lang(s_OperServ, u, OPER_EXCEPTION_ADDED, mask, limit); if (readonly) notice_lang(s_OperServ, u, READ_ONLY_MODE); } } /*************************************************************************/ static void do_exception_del(User *u) { char *mask; MaskData *except; int deleted = 0; mask = strtok(NULL, " "); if (!mask) { syntax_error(s_OperServ, u, "EXCEPTION", OPER_EXCEPTION_DEL_SYNTAX); return; } if (isdigit(*mask) && strspn(mask, "1234567890,-") == strlen(mask)) { int count, last = -1; deleted = process_numlist(mask, &count, exception_del_callback, u, &last); if (deleted == 0) { if (count == 1) { notice_lang(s_OperServ, u, OPER_EXCEPTION_NO_SUCH_ENTRY, last); } else { notice_lang(s_OperServ, u, OPER_EXCEPTION_NO_MATCH); } } else if (deleted == 1) { notice_lang(s_OperServ, u, OPER_EXCEPTION_DELETED_ONE); } else { notice_lang(s_OperServ, u, OPER_EXCEPTION_DELETED_SEVERAL, deleted); } } else { for (except = first_maskdata(MD_EXCEPTION); except; except = next_maskdata(MD_EXCEPTION)) { if (stricmp(mask, except->mask) == 0) { del_maskdata(MD_EXCEPTION, except); notice_lang(s_OperServ, u, OPER_EXCEPTION_DELETED, mask); deleted = 1; break; } } if (deleted == 0) notice_lang(s_OperServ, u, OPER_EXCEPTION_NOT_FOUND, mask); } if (deleted && readonly) notice_lang(s_OperServ, u, READ_ONLY_MODE); /* Renumber the exception list. I don't believe in having holes in * lists - it makes code more complex, harder to debug and we end up * with huge index numbers. Imho, fixed numbering is only beneficial * when one doesn't have range capable manipulation. -TheShadow */ /* That works fine up until two people do deletes at the same time * and shoot themselves in the collective foot; and just because * you have range handling doesn't mean someone won't do "DEL 5" * followed by "DEL 7" and, again, shoot themselves in the foot. * Besides, there's nothing wrong with complexity if it serves a * purpose. Removed. --AC */ } static int exception_del_callback(User *u, int num, va_list args) { MaskData *except; int *last = va_arg(args, int *); *last = num; if ((except = get_exception_by_num(num)) != NULL) { del_maskdata(MD_EXCEPTION, except); return 1; } else { return 0; } } /*************************************************************************/ static void do_exception_list(User *u, int is_view) { char *mask, *expiry; time_t expires; MaskData *except; int sent_header = 0; expires = -1; /* Do not match on expiry time */ mask = strtok(NULL, " "); if (mask) strlower(mask); expiry = strtok(NULL, " "); /* This is a little longwinded for what it acheives - but we can * expand it later to allow for user defined expiry times. */ if (expiry && stricmp(expiry, "NOEXPIRE") == 0) expires = 0; /* Exceptions that never expire */ if (mask && strspn(mask, "1234567890,-") == strlen(mask)) { process_numlist(mask, NULL, exception_list_callback, u, &sent_header, expires, is_view); } else { for (except = first_maskdata(MD_EXCEPTION); except; except = next_maskdata(MD_EXCEPTION) ) { if ((!mask || match_wild(mask, except->mask)) && (expires == -1 || except->expires == expires)) exception_list(u, except, &sent_header, is_view); } } if (!sent_header) notice_lang(s_OperServ, u, mask ? OPER_EXCEPTION_NO_MATCH : OPER_EXCEPTION_EMPTY); } static int exception_list(User *u, MaskData *except, int *sent_header, int is_view) { if (!*sent_header) { notice_lang(s_OperServ, u, OPER_EXCEPTION_LIST_HEADER); if (!is_view) notice_lang(s_OperServ, u, OPER_EXCEPTION_LIST_COLHEAD); *sent_header = 1; } if (is_view) { char timebuf[BUFSIZE], expirebuf[BUFSIZE]; strftime_lang(timebuf, sizeof(timebuf), u->ngi, STRFTIME_SHORT_DATE_FORMAT, except->time); expires_in_lang(expirebuf, sizeof(expirebuf), u->ngi, except->expires); notice_lang(s_OperServ, u, OPER_EXCEPTION_VIEW_FORMAT, except->num, except->mask, *except->who ? except->who : "", timebuf, expirebuf, except->limit, except->reason); } else { /* list */ notice_lang(s_OperServ, u, OPER_EXCEPTION_LIST_FORMAT, except->num, except->limit, except->mask); } return 1; } static int exception_list_callback(User *u, int num, va_list args) { int *sent_header = va_arg(args, int *); time_t expires = va_arg(args, time_t); int is_view = va_arg(args, int); MaskData *except; if (!(except = get_exception_by_num(num)) || (expires != -1 && except->expires != expires)) return 0; return exception_list(u, except, sent_header, is_view); } /*************************************************************************/ static void do_exception_move(User *u) { MaskData *except; char *n1str = strtok(NULL, " "); /* From index */ char *n2str = strtok(NULL, " "); /* To index */ int n1, n2; if (!n2str) { syntax_error(s_OperServ, u, "EXCEPTION", OPER_EXCEPTION_MOVE_SYNTAX); return; } n1 = atoi(n1str); n2 = atoi(n2str); if (n1 == n2 || n1 <= 0 || n2 <= 0) { syntax_error(s_OperServ, u, "EXCEPTION", OPER_EXCEPTION_MOVE_SYNTAX); return; } if (!(except = get_exception_by_num(n1))) { notice_lang(s_OperServ, u, OPER_EXCEPTION_NO_SUCH_ENTRY, n1); return; } except = move_exception(except, n2); notice_lang(s_OperServ, u, OPER_EXCEPTION_MOVED, except->mask, n1, n2); if (readonly) notice_lang(s_OperServ, u, READ_ONLY_MODE); } /*************************************************************************/ /*************************** Callback routines ***************************/ /*************************************************************************/ /* Callback to save exception data. */ static int do_save_data(void) { sync_exception_db(ExceptionDBName); return 0; } /*************************************************************************/ /* Callback to check session limiting for new users. */ static int check_sessions(int ac, char **av) { return !add_session(av[0], av[4]); } /*************************************************************************/ /* Callback to remove a quitting user's session. */ static int remove_session(User *u, char *reason_unused) { #ifdef CLEAN_COMPILE reason_unused = reason_unused; #endif del_session(u->host); return 0; } /*************************************************************************/ /* Callback for exception expiration. */ static int do_expire_maskdata(uint32 type, MaskData *md) { if (type == MD_EXCEPTION) { if (WallExceptionExpire) wallops(s_OperServ, "Session limit exception for %s has expired", md->mask); } return 0; } /*************************************************************************/ /* Callback for OperServ STATS ALL command. */ static int do_stats_all(User *u) { int32 count, mem; Session *session; count = mem = 0; for (session = first_session(); session; session = next_session()) { count++; mem += sizeof(*session) + strlen(session->host)+1; } notice_lang(s_OperServ, u, OPER_STATS_ALL_SESSION_MEM, count, (mem+512) / 1024); return 0; } /*************************************************************************/ /***************************** Module stuff ******************************/ /*************************************************************************/ const int32 module_version = MODULE_VERSION_CODE; ConfigDirective module_config[] = { { "DefSessionLimit", { { CD_INT, 0, &DefSessionLimit } } }, { "ExceptionDB", { { CD_STRING, CF_DIRREQ, &ExceptionDBName } } }, { "ExceptionExpiry", { { CD_TIME, 0, &ExceptionExpiry } } }, { "MaxSessionLimit", { { CD_POSINT, 0, &MaxSessionLimit } } }, { "SessionLimitAutokill",{{CD_SET, 0, &SessionLimitAutokill }, { CD_TIME, 0, &SessionLimitMinKillTime }, { CD_POSINT, 0, &SessionLimitMaxKillCount }, { CD_TIME, 0, &SessionLimitAutokillExpiry }, { CD_STRING, 0, &SessionLimitAutokillReason } } }, { "SessionLimitDetailsLoc",{{CD_STRING, 0, &SessionLimitDetailsLoc } } }, { "SessionLimitExceeded",{{CD_STRING, 0, &SessionLimitExceeded } } }, { "WallExceptionExpire",{{CD_SET, 0, &WallExceptionExpire } } }, { "WallOSException", { { CD_SET, 0, &WallOSException } } }, { NULL } }; /*************************************************************************/ static int do_load_module(Module *mod, const char *name) { if (strcmp(name, "operserv/akill") == 0) { p_create_akill = get_module_symbol(mod, "create_akill"); if (p_create_akill) module_akill = mod; else module_log("Unable to resolve symbol `create_akill' in module" " `operserv/akill'; automatic autokill addition will" " not be available"); } return 0; } /*************************************************************************/ static int do_unload_module(Module *mod) { if (mod == module_akill) { p_create_akill = NULL; module_akill = NULL; } return 0; } /*************************************************************************/ int init_module(Module *module_) { module = module_; if (!MaxSessionLimit) MaxSessionLimit = SESSION_MAXLIMIT; module_operserv = find_module("operserv/main"); if (!module_operserv) { module_log("Main OperServ module not loaded"); return 0; } use_module(module_operserv); if (!register_commands(module_operserv, cmds)) { module_log("Unable to register commands"); exit_module(0); return 0; } /* Add user check callback at priority -10 so it runs after all the * autokill/S-line/whatever checks (otherwise we get users added to * sessions and then killed by S-lines, leaving the session count * jacked up). */ if (!add_callback(NULL, "load module", do_load_module) || !add_callback(NULL, "unload module", do_unload_module) || !add_callback_pri(NULL, "user check", check_sessions, -10) || !add_callback(NULL, "user delete", remove_session) || !add_callback(NULL, "save data", do_save_data) || !add_callback(module_operserv, "expire maskdata", do_expire_maskdata) || !add_callback(module_operserv, "STATS ALL", do_stats_all) ) { module_log("Unable to add callbacks"); exit_module(0); return 0; } module_akill = find_module("operserv/akill"); if (module_akill) do_load_module(module_akill, "operserv/akill"); open_exception_db(ExceptionDBName); db_opened = 1; return 1; } /*************************************************************************/ int exit_module(int shutdown_unused) { Session *session; #ifdef CLEAN_COMPILE shutdown_unused = shutdown_unused; #endif if (db_opened) close_exception_db(ExceptionDBName); if (module_akill) do_unload_module(module_akill); for (session = first_session(); session; session = next_session()) { _del_session(session); free_session(session); } remove_callback(NULL, "save data", do_save_data); remove_callback(NULL, "user delete", remove_session); remove_callback(NULL, "user check", check_sessions); remove_callback(NULL, "unload module", do_unload_module); remove_callback(NULL, "load module", do_load_module); if (module_operserv) { remove_callback(module_operserv, "STATS ALL", do_stats_all); remove_callback(module_operserv, "expire maskdata",do_expire_maskdata); unregister_commands(module_operserv, cmds); unuse_module(module_operserv); module_operserv = NULL; } return 1; } /*************************************************************************/