/* Routines to maintain a list of online users. * * 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 "hash.h" /*************************************************************************/ /* Maximum number of tries to randomly select a new guest nick when the * first one chosen is in use before giving up. */ #define MAKEGUESTNICK_TRIES 1000 /*************************************************************************/ void add_user(User *user); /* to avoid "no prototype" warning */ void del_user(User *user); /* same */ DEFINE_HASH(user, User, nick) int32 usercnt = 0, opcnt = 0, maxusercnt = 0; time_t maxusertime; static int cb_check = -1; static int cb_create = -1; static int cb_servicestamp_change = -1; static int cb_nickchange1 = -1; static int cb_nickchange2 = -1; static int cb_delete = -1; static int cb_mode = -1; static int cb_chan_part = -1; static int cb_chan_kick = -1; /*************************************************************************/ int user_init(int ac, char **av) { cb_check = register_callback(NULL, "user check"); cb_create = register_callback(NULL, "user create"); cb_servicestamp_change=register_callback(NULL,"user servicestamp change"); cb_nickchange1 = register_callback(NULL, "user nickchange (before)"); cb_nickchange2 = register_callback(NULL, "user nickchange (after)"); cb_delete = register_callback(NULL, "user delete"); cb_mode = register_callback(NULL, "user MODE"); cb_chan_part = register_callback(NULL, "channel PART"); cb_chan_kick = register_callback(NULL, "channel KICK"); if (cb_check < 0 || cb_create < 0 || cb_servicestamp_change < 0 || cb_nickchange1 < 0 || cb_nickchange2 < 0 || cb_delete < 0 || cb_mode < 0 || cb_chan_part < 0 || cb_chan_kick < 0 ) { log("user_init: register_callback() failed\n"); return 0; } return 1; } /*************************************************************************/ void user_cleanup(void) { User *u; for (u = first_user(); u; u = next_user()) del_user(u); unregister_callback(NULL, cb_chan_kick); unregister_callback(NULL, cb_chan_part); unregister_callback(NULL, cb_mode); unregister_callback(NULL, cb_delete); unregister_callback(NULL, cb_nickchange2); unregister_callback(NULL, cb_nickchange1); unregister_callback(NULL, cb_servicestamp_change); unregister_callback(NULL, cb_create); unregister_callback(NULL, cb_check); } /*************************************************************************/ /************************* User list management **************************/ /*************************************************************************/ /* Allocate a new User structure, fill in basic values, link it to the * overall list, and return it. Always successful. */ static User *new_user(const char *nick) { User *user; user = scalloc(sizeof(User), 1); if (!nick) nick = ""; strscpy(user->nick, nick, NICKMAX); add_user(user); usercnt++; if (usercnt > maxusercnt) { maxusercnt = usercnt; maxusertime = time(NULL); if (LogMaxUsers) log("user: New maximum user count: %d", maxusercnt); } return user; } /*************************************************************************/ /* Change the nickname of a user, and move pointers as necessary. */ static void change_user_nick(User *user, const char *nick) { del_user(user); strscpy(user->nick, nick, NICKMAX); add_user(user); } /*************************************************************************/ /* Remove and free a User structure. */ static void delete_user(User *user) { struct u_chanlist *c, *c2; struct u_chaninfolist *ci, *ci2; usercnt--; if (is_oper(user)) opcnt--; free(user->username); free(user->host); free(user->ipaddr); free(user->realname); free(user->fakehost); free(user->id_nicks); LIST_FOREACH_SAFE (c, user->chans, c2) { chan_deluser(user, c->chan); free(c); } LIST_FOREACH_SAFE (ci, user->id_chans, ci2) free(ci); #define next snext #define prev sprev if (user->server) LIST_REMOVE(user, user->server->userlist); #undef next #undef prev del_user(user); free(user); } /*************************************************************************/ /*************************************************************************/ /* Remove a user on QUIT/KILL. Calls the user delete callback and then * deletes the User structure. */ void quit_user(User *user, const char *quitmsg, int is_kill) { call_callback_3(NULL, cb_delete, user, quitmsg, is_kill); delete_user(user); } /*************************************************************************/ /* Return statistics. Pointers are assumed to be valid. */ void get_user_stats(long *nusers, long *memuse) { long count = 0, mem = 0; User *user; struct u_chanlist *uc; struct u_chaninfolist *uci; for (user = first_user(); user; user = next_user()) { count++; mem += sizeof(*user); if (user->username) mem += strlen(user->username)+1; if (user->host) mem += strlen(user->host)+1; if (user->realname) mem += strlen(user->realname)+1; LIST_FOREACH (uc, user->chans) mem += sizeof(*uc); LIST_FOREACH (uci, user->id_chans) mem += sizeof(*uci); } *nusers = count; *memuse = mem; } /*************************************************************************/ /************************* Internal routines *****************************/ /*************************************************************************/ /* Part a user from a channel given the user's u_chanlist entry for the * channel. */ static void part_channel_uc(User *user, struct u_chanlist *uc, int callback, const char *param) { call_callback_3(NULL, callback, uc->chan, user, param); chan_deluser(user, uc->chan); LIST_REMOVE(uc, user->chans); free(uc); } /*************************************************************************/ /************************* Message handlers ******************************/ /*************************************************************************/ /* Handle a server NICK command. Parameters must be in the following order. * av[0] = nick * If a new user: * av[1] = hop count * av[2] = signon time * av[3] = username * av[4] = hostname * av[5] = server * av[6] = real name * av[7] = services stamp (if ac >= 8; NULL if none) * av[8] = IP address (if ac >= 9; NULL if unknown) * av[9..] available for protocol module use * Else: * av[1] = time of change * Return 1 if message was accepted, 0 if rejected (AKILL/session limit). */ int do_nick(const char *source, int ac, char **av) { User *user; if (!*source) { /* This is a new user; create a User structure for it. */ if (debug) log("debug: new user: %s", av[0]); /* We used to ignore the ~ which a lot of ircd's use to indicate no * identd response. That caused channel bans to break, so now we * just take what the server gives us. People are still encouraged * to read the RFCs and stop doing anything to usernames depending * on the result of an identd lookup. */ /* First check whether the user should be allowed on. */ if (call_callback_2(NULL, cb_check, ac, av)) return 0; /* User was accepted; allocate User structure and fill it in. */ user = new_user(av[0]); user->my_signon = time(NULL); user->signon = strtotime(av[2], NULL); user->username = sstrdup(av[3]); user->host = sstrdup(av[4]); user->server = get_server(av[5]); user->realname = sstrdup(av[6]); if (ac >= 8 && av[7]) user->servicestamp = strtoul(av[7], NULL, 10); else user->servicestamp = (uint32)user->signon; if (ac >= 9 && av[8]) user->ipaddr = sstrdup(av[8]); else user->ipaddr = NULL; #define next snext #define prev sprev if (user->server) LIST_INSERT(user, user->server->userlist); #undef next #undef prev call_callback_3(NULL, cb_create, user, ac, av); if (ac >= 8 && av[7] && !user->servicestamp) { /* A servicestamp was provided, but it was zero, so assign one. * Note that we use a random value for the initial Services * stamp instead of the current time for the following reason: * * Suppose you have a network with an average of more than one * new user per second; for the sake of argument, assume there * are an average of 1.3 new users per second. If the initial * Services stamp is T, the current time, then in 100 seconds * (i.e. at T+100) the Services stamp will have gone to T+130. * (In reality, it would jump much higher on the initial net * burst when no users have Services stamps, but that does not * affect this argument.) * * If Services is now restarted, clearing the last used stamp * value, then assuming 5 seconds for restart, Services will * receive a network burst at T+105. However! While most of * the users will already have Services stamps, any new users * (as well as any users which connect after the network burst) * will be assigned new Services stamps starting with the * default value of the current time, in this case T+105. But * other users _already_ have Services stamp values in the * range T+105 to T+130 from the previous run--thus you have * Services stamp collisions, and all the security problems * that go with them. * * Obviously, this possibility does not disappear entirely by * using a random initial value, but it becomes much more * unlikely. * * Note that Unreal 3.1.1 upper-bounds values at 2^31-1, so we * limit ourselves to 31 bits here, even though our field is * unsigned. */ static int32 servstamp = 0; if (servstamp == 0) servstamp = (rand() & 0x7FFFFFFF) | 1; user->servicestamp = servstamp++; if (servstamp <= 0) servstamp = 1; call_callback_1(NULL, cb_servicestamp_change, user); } } else { /* An old user changing nicks. */ char oldnick[NICKMAX]; user = get_user(source); if (!user) { log("user: NICK from nonexistent nick %s: %s", source, merge_args(ac, av)); return 0; } if (debug) log("debug: %s changes nick to %s", source, av[0]); strscpy(oldnick, user->nick, NICKMAX); call_callback_2(NULL, cb_nickchange1, user, av[0]); /* Flush out all mode changes; necessary to avoid desynch (otherwise * we can't find the user when the mode goes out later). The IRC * servers will take care of translating the old nick to the new one */ set_cmode(NULL, NULL); change_user_nick(user, av[0]); call_callback_2(NULL, cb_nickchange2, user, oldnick); } return 1; } /*************************************************************************/ /* Handle a JOIN command. * av[0] = channels to join */ void do_join(const char *source, int ac, char **av) { User *user; char *s, *t; user = get_user(source); if (!user) { log("user: JOIN from nonexistent user %s: %s", source, merge_args(ac, av)); return; } t = av[0]; while (*(s=t)) { t = s + strcspn(s, ","); if (*t) *t++ = 0; if (debug) log("debug: %s joins %s", source, s); if (*s == '0') part_all_channels(user); else join_channel(user, s, 0); } } /*************************************************************************/ /* Handle a PART command. * av[0] = channels to leave * av[1] = reason (optional) */ void do_part(const char *source, int ac, char **av) { User *user; char *s, *t; user = get_user(source); if (!user) { log("user: PART from nonexistent user %s: %s", source, merge_args(ac, av)); return; } t = av[0]; while (*(s=t)) { t = s + strcspn(s, ","); if (*t) *t++ = 0; if (debug) log("debug: %s leaves %s", source, s); if (!part_channel(user, s, cb_chan_part, av[1])) { log("user: do_part: no channel record for %s on %s (bug?)", user->nick, av[0]); } } } /*************************************************************************/ /* Handle a KICK command. * av[0] = channel * av[1] = nick(s) being kicked * av[2] = reason * When called internally to remove a single user (no "," in av[1]) from a * channel, callers may assume that the contents of the argument strings * will not be modified. */ void do_kick(const char *source, int ac, char **av) { User *user; char *s, *t; t = av[1]; while (*(s=t)) { t = s + strcspn(s, ","); if (*t) *t++ = 0; user = get_user(s); if (!user) { log("user: KICK for nonexistent user %s on %s: %s", s, av[0], merge_args(ac-2, av+2)); continue; } if (debug) log("debug: kicking %s from %s", s, av[0]); if (!part_channel(user, av[0], cb_chan_kick, av[2])) { log("user: do_kick: no channel record for %s on %s (bug?)", user->nick, av[0]); } } } /*************************************************************************/ /* Handle a MODE command for a user. * av[0] = nick to change mode for * av[1] = modes */ void do_umode(const char *source, int ac, char **av) { User *user; char *modestr, *s; int add = 1; /* 1 if adding modes, 0 if deleting */ user = get_user(av[0]); if (!user) { log("user: MODE %s for nonexistent nick %s from %s: %s", av[1], av[0], source, merge_args(ac, av)); return; } if (debug) log("debug: Changing mode for %s to %s", av[0], av[1]); modestr = s = av[1]; av += 2; ac -= 2; while (*s) { char modechar = *s++; int32 flag; int params; if (modechar == '+') { add = 1; continue; } else if (modechar == '-') { add = 0; continue; } else if (add < 0) { continue; } flag = mode_char_to_flag(modechar, MODE_USER); if (!flag) continue; if (flag == MODE_INVALID) flag = 0; params = mode_char_to_params(modechar, MODE_USER); params = (params >> (add*8)) & 0xFF; if (ac < params) { log("user: MODE %s %s: missing parameter(s) for %c%c", user->nick, modestr, add ? '+' : '-', modechar); break; } if (call_callback_4(NULL, cb_mode, user, modechar, add, av) <= 0) { if (modechar == 'o') { if (add) opcnt++; else opcnt--; } if (add) user->mode |= flag; else user->mode &= ~flag; } av += params; ac -= params; } } /*************************************************************************/ /* Handle a QUIT command. * av[0] = reason * When called internally, callers may assume that the contents of the * argument string will not be modified. */ void do_quit(const char *source, int ac, char **av) { User *user; user = get_user(source); if (!user) { log("user: QUIT from nonexistent user %s: %s", source, merge_args(ac, av)); return; } if (debug) log("debug: %s quits", source); quit_user(user, av[0], 0); } /*************************************************************************/ /* Handle a KILL command. * av[0] = nick being killed * av[1] = reason * When called internally, callers may assume that the contents of the * argument strings will not be modified. */ void do_kill(const char *source, int ac, char **av) { User *user; user = get_user(av[0]); if (!user) return; if (debug) log("debug: %s killed", av[0]); quit_user(user, av[1], 1); } /*************************************************************************/ /*************************************************************************/ /* Join a user to a channel. Return a pointer to the channel record if the * join succeeded, NULL otherwise. */ Channel *join_channel(User *user, const char *channel, int32 modes) { Channel *c = chan_adduser(user, channel, modes); struct u_chanlist *uc; if (!c) return NULL; uc = smalloc(sizeof(*uc)); LIST_INSERT(uc, user->chans); uc->chan = c; return c; } /*************************************************************************/ /* Part a user from a channel. */ int part_channel(User *user, const char *channel, int callback, const char *param) { struct u_chanlist *uc; LIST_SEARCH(user->chans, chan->name, channel, irc_stricmp, uc); if (uc) part_channel_uc(user, uc, callback, param); return uc != NULL; } /*************************************************************************/ /* Part a user from all channels s/he is in. Assumes cb_chan_part and an * empty parameter. */ void part_all_channels(User *user) { struct u_chanlist *uc, *nextuc; LIST_FOREACH_SAFE (uc, user->chans, nextuc) part_channel_uc(user, uc, cb_chan_part, ""); } /*************************************************************************/ /*************************************************************************/ /* Various check functions. All of these return false/NULL if any * parameter is NULL. */ /*************************************************************************/ /* Is the given user an oper? */ int is_oper(const User *user) { return user != NULL && (user->mode & UMODE_o); } /*************************************************************************/ /* Is the given user on the given channel? Return the Channel * for the * channel if so, NULL if not. */ Channel *is_on_chan(const User *user, const char *chan) { struct u_chanlist *c; if (!user || !chan) return NULL; LIST_SEARCH(user->chans, chan->name, chan, irc_stricmp, c); return c ? c->chan : NULL; } /*************************************************************************/ /* Is the given user a channel operator on the given channel? */ int is_chanop(const User *user, const char *chan) { Channel *c = chan ? get_channel(chan) : NULL; struct c_userlist *cu; if (!user || !chan || !c) return 0; LIST_SEARCH(c->users, user->nick, user->nick, irc_stricmp, cu); return cu != NULL && (cu->mode & CUMODE_o) != 0; } /*************************************************************************/ /* Is the given user voiced (channel mode +v) on the given channel? */ int is_voiced(const User *user, const char *chan) { Channel *c = chan ? get_channel(chan) : NULL; struct c_userlist *cu; if (!user || !chan || !c) return 0; LIST_SEARCH(c->users, user->nick, user->nick, irc_stricmp, cu); return cu != NULL && (cu->mode & CUMODE_v) != 0; } /*************************************************************************/ /*************************************************************************/ /* Does the user's usermask match the given mask (either nick!user@host or * just user@host)? Also checks fakehost where supported. */ int match_usermask(const char *mask, const User *user) { char *mask2 = sstrdup(mask); char *nick, *username, *host; int match_user, match_host, result; if (strchr(mask2, '!')) { nick = strtok(mask2, "!"); username = strtok(NULL, "@"); } else { nick = NULL; username = strtok(mask2, "@"); } host = strtok(NULL, ""); if (!host) { free(mask2); return 0; } match_user = match_wild_nocase(username, user->username); match_host = match_wild_nocase(host, user->host); if (user->fakehost) match_host |= match_wild_nocase(host, user->fakehost); if (nick) { result = match_wild_nocase(nick, user->nick) && match_user && match_host; } else { result = match_user && match_host; } free(mask2); return result; } /*************************************************************************/ /* Split a usermask up into its constitutent parts. Returned strings are * malloc()'d, and should be free()'d when done with. Returns "*" for * missing parts. Assumes `mask' is a non-empty string. */ void split_usermask(const char *mask, char **nick, char **user, char **host) { char *mask2 = sstrdup(mask); char *mynick, *myuser, *myhost; mynick = mask2; myuser = strchr(mask2, '!'); myhost = myuser ? strchr(myuser, '@') : NULL; if (myuser) *myuser++ = 0; if (myhost) *myhost++ = 0; /* Handle special case: mask == user@host */ if (mynick && !myuser && strchr(mynick, '@')) { mynick = NULL; myuser = mask2; myhost = strchr(mask2, '@'); if (myhost) /* Paranoia */ *myhost++ = 0; } if (!mynick || !*mynick) mynick = (char *)"*"; if (!myuser || !*myuser) myuser = (char *)"*"; if (!myhost || !*myhost) myhost = (char *)"*"; *nick = sstrdup(mynick); *user = sstrdup(myuser); *host = sstrdup(myhost); free(mask2); } /*************************************************************************/ /* Given a user, return a mask that will most likely match any address the * user will have from that location. For IP addresses, wildcards the last * octet of the address (e.g. 10.1.1.1 -> 10.1.1.*); for named addresses, * wildcards the leftmost part of the name unless the name only contains * two parts. The returned character string is malloc'd and should be * free'd when done with. * * Where supported, uses the fake host instead of the real one if * use_fakehost is nonzero. */ char *create_mask(User *u, int use_fakehost) { char *mask, *s, *end, *host; host = u->host; if (use_fakehost && u->fakehost) host = u->fakehost; /* Get us a buffer the size of the username plus hostname. The result * will never be longer than this (and will often be shorter), thus we * can use strcpy() and sprintf() safely. */ end = mask = smalloc(strlen(u->username) + strlen(host) + 2); end += sprintf(end, "%s@", u->username); if (strspn(host, "0123456789.") == strlen(host) && (s = strchr(host, '.')) && (s = strchr(s+1, '.')) && (s = strchr(s+1, '.')) && ( !strchr(s+1, '.'))) { /* IP addr */ s = sstrdup(host); *strrchr(s, '.') = 0; sprintf(end, "%s.*", s); free(s); } else { if ((s = strchr(host+1, '.')) && strchr(s+1, '.')) { s = sstrdup(s-1); *s = '*'; } else { s = sstrdup(host); } strcpy(end, s); /* safe: see above */ free(s); } return mask; } /*************************************************************************/ /*************************************************************************/ /* Create a new guest nick using GuestNickPrefix and a unique series of * digits, and return it. The returned nick is stored in a static buffer * and will be overwritten at the next call. * * At present, we simply use a rollover counter attached to the nick, * initialized to a random value. This provides for uniqueness as long as * Services is not restarted and a reasonable chance of uniqueness even * when Services is restarted (unless you have a long prefix and a short * maximum nick length; however, this routine will make sure at least 4 * digits are available, possibly by shortening the prefix). * * Note that initializing the counter based on the current time would be a * bad idea, since if more than one user per second (on average) connected * to the network, duplicate nicks would be almost guaranteed if Services * restarted. */ char *make_guest_nick(void) { static char nickbuf[NICKMAX+1]; /* +1 to check for overrun */ static uint32 counter = 0; /* Unique suffix counter */ int tries; /* Tries to find an unused nick */ int prefixlen; /* Length of nick prefix */ uint32 suffixmod; /* Modulo for suffix counter */ int i; /* Sanity checks on nick prefix length */ prefixlen = strlen(GuestNickPrefix); if (protocol_nickmax <= 4) { /* This violates RFC1459 as well as common sense, so just blow * ourselves out of the water. */ fatal("make_guest_nick(): protocol_nickmax too small (%d)", protocol_nickmax); } else if (prefixlen+4 > protocol_nickmax) { /* Reserve at least 4 digits for the suffix */ prefixlen = protocol_nickmax-4; log("warning: make_guest_nick(): GuestNickPrefix too long," " shortening to %d characters", prefixlen); GuestNickPrefix[prefixlen] = 0; } /* Calculate number of digits available for suffix -> suffix modulo */ i = protocol_nickmax - prefixlen; if (i < 10) { suffixmod = 1; while (i-- > 0) suffixmod *= 10; } else { suffixmod = 0; /* no modulo */ } /* Actually generate the nick. If the nick already exists, generate a * new one, and repeat until finding an unused nick or trying too many * times. If we try too many times, we just kill the last one we end * up with. */ tries = 0; for (;;) { if (counter == 0) /* initialize to random the first time */ counter = rand(); if (suffixmod) /* strip down to right number of digits */ counter %= suffixmod; if (counter == 0) /* if rand() gave us 0 or N*suffixmod... */ counter = 1; /* ... use 1 instead */ i = snprintf(nickbuf, sizeof(nickbuf), "%s%u", GuestNickPrefix, counter); if (i > protocol_nickmax) { log("BUG: make_guest_nick() generated %s but nickmax == %d!", nickbuf, protocol_nickmax); nickbuf[protocol_nickmax] = 0; } if (!get_user(nickbuf)) /* done if user not found */ break; /* Increment try count, and break out if it's too much */ if (++tries >= MAKEGUESTNICK_TRIES) { kill_user(NULL, nickbuf, "Guest nicks may not be used"); break; } /* Reset counter to 0 to use a new random number */ counter = 0; } /* Increment the unique suffix counter modulo suffixmod, and avoid 0 * (which would cause a reset to a random value) */ counter++; if (counter == suffixmod) /* because suffixmod==0 if no modulo */ counter = 1; /* Return the nick */ return nickbuf; }; /*************************************************************************/ /* Return nonzero if the given nickname can potentially be a guest nickname * (i.e. a nickname that could be generated by make_guest_nick()), zero if * not. Currently this means a nickname consisting of GuestNickPrefix * followed by between 1 and 10 digits inclusive. */ int is_guest_nick(const char *nick) { int prefixlen = strlen(GuestNickPrefix); int nicklen = strlen(nick); return nicklen >= prefixlen+1 && nicklen <= prefixlen+10 && irc_strnicmp(nick, GuestNickPrefix, prefixlen) == 0 && strspn(nick+prefixlen, "1234567890") == nicklen-prefixlen; } /*************************************************************************/