IRC Services Technical Reference Manual

7. Services pseudoclients

7-1. Basic features of a pseudoclient
7-2. OperServ
    7-2-1. OperServ core functionality
    7-2-2. Usermask-related functions
        7-2-2-1. Common mask data support
        7-2-2-2. Autokills
        7-2-2-3. S-lines
    7-2-3. Session limiting
    7-2-4. News
7-3. NickServ
    7-3-1. NickServ core functionality
        7-3-1-1. Nickname data structures and utility macros
        7-3-1-2. Overall module structure
        7-3-1-3. The SET and UNSET commands
        7-3-1-4. NickServ utility routines
        7-3-1-5. Nickname colliding
    7-3-2. Nickname access lists
    7-3-3. Nickname auto-join lists
    7-3-4. Linking and nickname groups
    7-3-5. E-mail address authentication
7-4. ChanServ
    7-4-1. ChanServ core functionality
        7-4-1-1. Channel data structures
        7-4-1-2. Overall module structure
        7-4-1-3. Channel status checking and modification
        7-4-1-4. The SET and UNSET commands
        7-4-1-5. ChanServ utility routines
    7-4-2. Channel access list handling
        7-4-2-1. Access list basics
        7-4-2-2. Manipulation via ACCESS and LEVELS
        7-4-2-3. Manipulation via XOP
7-5. MemoServ
    7-5-1. MemoServ core functionality
        7-5-1-1. Memo data structures
        7-5-1-2. The memoserv/main module
    7-5-2. Memo ignore lists
    7-5-3. Memo forwarding
7-6. StatServ
    7-6-1. StatServ data structures
    7-6-2. The StatServ module
7-7. Miscellaneous pseudoclients
    7-7-1. HelpServ
    7-7-2. DevNull


7-1. Basic features of a pseudoclient

Pseudoclients are the user-visible part of Services, providing the actual service functions which IRC users take advantage of. While the details of each pseudoclient differ greatly, all pseudoclients share some common features:

The "introduce_user" callback mentioned above is called by the same-named function, introduce_user(), to request that each module introduce its nickname(s) to the IRC network by calling send_pseudo_nick() (in send.c). The callback takes a single const char * parameter, which is NULL when the callback is called at startup, or the nickname seen in a KILL message when one is received; accordingly, pseudoclients should introduce their nicknames only when the parameter either is NULL or matches (case-insensitively, as determined by irc_stricmp()) the nickname to be introduced. send_pseudo_nick() takes three parameters: the nickname of the pseudoclient to be introduced, the pseudoclient's "real name" string (usually a description of the pseudoclient), and a flag value, composed of zero or more of the following flags:

Command processing is typically performed by hooking into the core's "m_privmsg" callback, which is intended specifically for this purpose. Depending on the pseudoclient, it may also be necessary to respond to other events on the IRC network, such as channel joins or mode changes; these can typically be handled by hooking into the relevant callback. In extreme cases, it may be necessary to make use of the low-level "receive message" callback as well; this should be avoided when possible, however, as it circumvents the standard message processing and can result in network desynchronization.

For databases maintained by pseudoclients, the following six functions are typically provided by the pseudoclient for other code that needs direct access to the database (type is the record data type, name is a distinguishing name generally derived from type, and keytype is the data type of the key field):

type *add_name(type *record)
Adds the given record to the database, returning a pointer to the record structure as stored (which may be different from the pointer passed into the function).
void del_name(type *record)
Deletes the given record from the database. record is assumed to be valid, i.e. a pointer returned by a previous call to another database function.
type *get_name(keytype *key)
Returns the record for the given key, or NULL if the key is not found in the database.
void put_name(type *record)
Indicates that the given record is no longer in use. Each call to a database's get() function must be followed by a put() call for the same record, unless the record is deleted first. Implementation note: A put() function was first introduced in 5.0 with the intention that it be used for indicating when a record had been updated, for the purpose of storing the updated data into persistent storage such as an SQL-based database. DBMS support never materialized, and the function's purpose was redefined for version 5.1 to keep track of which records are actively in use, but I have no confidence that the code does put() calls in all necessary cases and no others; in fact, it appears that put_nickgroupinfo() (at least) is called more often than it should be. It may have been better to drop put() entirely and use other methods of checking whether records are in use before expiring them.
type *first_name()
type *next_name()
Iterates through the database. first() returns the first record in the database, and subsequent next() calls return each successive record, finally returning NULL when all records have been iterated through (if there are no records at all, both first() and next() return NULL). The order is database-dependent, but no record will be returned more than once from the time first() is called to the time next() returns NULL. It is unspecified whether records added while iterating through the database will be included in the iteration. Note that these functions are not considered a "get" for the purposes of the put() function; thus a call to put() is not required for simple iteration, but if any other database function is to be called before the following next(), then an explicit call to get() (and a matching call to put()) must be made to prevent the record in use from being deleted.

The following sections describe each of the Services pseudoclients in detail. The reader is assumed to be familiar with the functions of each pseudoclient from a user's point of view, as described in section 3 of the user's manual.


7-2. OperServ

The OperServ pseudoclient provides services to IRC operators allowing control of the network and of Services itself. While not seen by most IRC users, this pseudoclient is discussed first since it provides functionality used by most other pseudoclients.

As with most of the Services pseudoclients, OperServ is composed of one core or "main" module, operserv/main, and several optional modules providing additional functionality. These are each discussed in separate sections below.

7-2-1. OperServ core functionality

The core functionality of OperServ is contained in the modules/operserv/main.c source file, compiled into the operserv/main module. In addition to the implementation of the core OperServ commands, this file also defines several utility functions used by several other pseudoclient modules; external declarations of these functions and associated constants are located in modules/operserv/operserv.h.

main.c is written using the same general structure as most module source files. At the top of the file are variable definitions, including configuration variables, which are given the same names as their corresponding configuration directives; these are followed by forward declarations of individual command routines and the command list. Next come database-related structures and routines, followed by the top-level pseudoclient routines (the "introduce_user", "m_privmsg", and "m_whois" callback functions), and finally the actual command routines, along with any utility routines needed.

For OperServ, the first item of note is the list of several commands and command routines inside #ifdef DEBUG_COMMANDS. These are, as the conditional name suggests, commands used for debugging Services, and are only available to the Services super-user; the commands are described in detail below.

OperServ stores several values to persistent storage, including the maximum client count, the time at which that maximum was reached, and the super-user (SU command) password. This data is stored by aggregating the data into a single structure, operserv_data, and storing that structure as a single database "record" in a table named "oper". Two exported functions, get_operserv_data() and put_operserv_data(), are also provided to allow external modules, in particular the XML import and export modules (see section 8-4), to access the data as well.

In order to check a client's Services privilege level (Services operator, Services administrator, or Services super-user), OperServ requires access to the nickname data, in which each registered nickname group's privilege level is stored (the os_priv member). However, since NickServ requires that OperServ be loaded first, OperServ must look up the symbols for these routines during its normal operation. Six local functions are defined, one for each of the imported routines (get_nickinfo(), put_nickinfo(), _get_ngi(), put_nickgroupinfo(), first_nickgroupinfo(), and next_nickgroupinfo()), taking the same parameters and returning the same values as the real routines; the local versions look up the symbol for each routine and then call the corresponding address, returning an appropriate error value if the symbol cannot be resolved (or NickServ is not loaded).

The main processing routine itself, operserv(), is registered as a callback function for the "m_privmsg" callback, called for each PRIVMSG received by Services. The routine first checks that the message is intended for OperServ; then it ensures that the client that sent the message is an IRC operator, to avoid any possibility of non-operator clients exploiting a bug in the OperServ code. The message received is then logged in the log file, except that parameters to the SU and SET SUPASS commands are replaced with a dummy string to avoid leaving the super-user password in the log file. (OperServ has no way to detect if one of these commands is misspelled, so for example, a mistaken SET SUPSAS will be logged in full, including the password.) Next, the command name is extracted from the message using the strtok() function; this also prepares strtok() for the command handler to use in extracting the command parameters (see below). After handling CTCP PING messages separately, OperServ calls a "command" callback, allowing other modules a chance to process the command first. Finally, if no callback function responds to the command, it is looked up in the command table and the command's handler function is called (if the command is not found, an error message is sent instead).

Following this and other callback functions are the privilege check functions; is_services_root(), is_services_admin(), and is_services_oper(), which check whether a client has Services super-user, Services administrator, and Services operator privileges respectively (any client with a higher privilege level is treated as having all lower privilege levels as well). The is_services_root() function relies on the ServicesRoot configuration setting, along with the UF_SERVROOT flag in the User structure indicating clients which have successfully used the SU command; lower privilege levels check the os_priv field of the nickname group data structure (see section 7-3-1) for privilege determination. These routines are exported, and used widely throughout the other pseudoclient modules to perform privilege checks on clients; in particular, they can be used as privilege check functions in the has_priv member of a Command structure. There is another routine, nick_is_services_admin(), which checks if a particular nickname can potentially can Services administrator access, ignoring whether the nickname is actually in use at the time; this is used by NickServ to prevent certain operations from being performed on such nicknames by clients without Services administrator privilege.

The command routines themselves are fairly straightforward. One thing to note is that the routines all obtain parameters via strtok(NULL,...) and strtok_remaining(); this relies on the fact that operserv() leaves the message string in the strtok() buffer after stripping the command name, so that each routine can parse the command's parameters as appropriate for that command. strtok_remaining() is used when the full remaining string is desired, such as when sending a global message; this function is preferred to strtok(NULL,"") because the latter can leave leading or trailing whitespace in the result.

The processing for the HELP command, in do_help(), is somewhat tortuous (although still simpler than in other pseudoclients) in order to give proper help responses depending on how Services is configured. In the case of OperServ, some commands may or may not be available depending on what submodules are loaded; the COMMANDS help text, which lists the available commands, is combined from several language strings depending on whether the appropriate modules are available to provide a list of the commands which are actually usable at that particular time. Other modules include more complex processing, such as checking the configuration variable values or protocol features. For commands that do not need such special-casing, the help_cmd() routine in the Services core (see section 2-10) sends a help message as defined by the Command structure.

The debug commands, defined toward the bottom of main.c, are as follows:

LISTSERVERS (send_server_list())
Sends a NOTICE for each server in the server list giving the contents of the Server structure.
LISTCHANS (send_channel_list())
Sends two NOTICEs for each channel in the channel list. The first NOTICE gives the channel name, creation time, modes (as a string), limit, key ("-") if none, and topic; the second contains each client on the channel along with that client's channel user modes on the channel. Any messages which exceed the maximum length of an IRC line are silently truncated.
LISTCHAN channel (send_channel_info())
Sends a NOTICE for each client on the given channel with the client's channel user modes (as a hexadecimal value) and nickname.
LISTUSERS (send_user_list())
Sends a NOTICE for each client on the network, giving the client's nickname and usermask, the "fake host" or "-" if none, the IP address or "-" if none, user modes as a string, signon timestamp (from the remote server), servicestamp, server name, nickname status flags or "-" if the nickname is not registered, ignore value, and real name field.
LISTUSER nickname (send_user_info())
Sends three NOTICEs describing the state of the given client. The first is identical to the line that LISTUSERS would output for the client; the second gives the channels which the client has currently joined; and the third gives the channels which the client has identified to ChanServ for.
LISTTIMERS (send_timeout_list(), in timeout.c)
Sends a NOTICE giving the current time, followed by a NOTICE for each timeout currently in the list giving the timeout pointer, timestamp, function pointer, and function argument. This routine is defined in timeout.c because the internal timeout fields are hidden from other source files.
MATCHWILD pattern string (do_matchwild())
Sends a NOTICE giving the result of calling match_wild with the given parameters.
SETCMODE [channel modes mode-params...] (do_setcmode())
Calls set_cmode() with the given parameters, using ServerName as the message sender. If no parameters are given, calls set_cmode(NULL,NULL) to flush out all pending mode changes. Note that the number of mode parameters (including the mode string itself) is limited by the SETCMODE_NPARAMS macro, defined to 10 in the source code.
MONITOR-IGNORE [nickname] (do_monitor_ignore())
If a nickname is given, starts recording that nickname's ignore value to the log file at 100ms intervals; if no nickname is given, cancels any previous monitoring. Note that in order to ensure sub-second resolution, the TimeoutCheck configuration variable is set to 10 (milliseconds) when a nickname is given, potentially causing a reduction in performance; the old value is not restored when the command is given without a nickname.
GETSTRING language string (do_getstring())
Sends a NOTICE containing the text corresponding to the given string in the given language. Both language and string can be given as either names or raw numbers.
SETSTRING language string [text] (do_setstring())
Calls setstring() to set the given string in the given language to the given text. If no text is given, the string becomes empty.
MAPSTRING old new (do_mapstring())
Calls mapstring() to map one string to another. old and new are string names or numbers.
ADDSTRING name (do_addstring())
Calls addstring() to add a string with the given name to the language table. On success, sends a NOTICE with the new string number.

7-2-2. Usermask-related functions

7-2-2-1. Common mask data support

One of the major features of OperServ not included in the core module is the autokill and S-line functionality. While these are in fact defined in separate modules, most of the processing for both sets of functions is subsumed under the MaskData structure and its generic processing routines included in the core OperServ module, located in maskdata.c and maskdata.h. This structure contains the elements common to all of these features: a mask string (whose interpretation is left to the particular module), an associated reason string, the nickname of the client that added the mask, the time the mask was added, the time it expires, and the last time the mask was applied to a client; these are stored in the mask, reason, who, time, expires, and lastused fields of the structure, respectively. The remaining fields (other than the record management fields next, prev, and usecount) are: type, containing an 8-bit value identifying the type of mask; num, giving the user-visible index number for session exception records (see section 7-2-3); and limit, used for the session limit in session exception records.

As mentioned above, each MaskData record has an associated type; more accurately, there are multiple sets of MaskData records, one set for each type (the type field is only used internally for loading and saving data from or to persistent storage, and does not need to be set by the caller). The available types, defined by the MD_* constants in maskdata.h, are:

It is worth noting that (as also mentioned in section 7-2-2-3 below) the values chosen for MD_SGLINE, MD_SQLINE, and MD_SZLINE are the ASCII values of the characters G, Q, and Z respectively; this was done in order to simplify common code for all three types of S-lines, so that the type value could be used as is in messages sent to clients rather than having to make separate tests to determine the appropriate text to send. (For example, most of the language file messages for S-line actions use "S%cLINE" in the format string, where the %c is replaced by the type value.)

One other structure, MaskDataCmdInfo, can be found in maskdata.h; this structure collects information particular to a single MaskData type for use by the common command processing. The bulk of the structure consists of language string indices, which are used in sending responses to commands procesed by the common code. These are preceded by: name, the command name (such as "AKILL" or "SGLINE"); type, the record type to be used; and def_expiry_ptr, a pointer to a variable containing the default expiration period in seconds (a pointer is used so that any changes to the value can be recognized immediately without having to modify this structure as well). There are also five function pointers, all of which are optional (and should be set to NULL if not needed):

void (*mangle_mask)(char *mask)
Makes any necessary changes to a mask before it is operated on. Any modifications may be made to the mask as long as they do not lengthen it beyond the original string length. Currently, this is used to force case-insensitive masks to lowercase, to avoid the possibility of multiple matching masks with differing case from being added to the database.
int (*check_add_mask)(const User *u, uint8 type, char *mask, time_t *expiry_ptr)
Checks whether the given mask of the given type is allowed to be added with the given expiration time (in seconds from the present time, passed as a pointer); returns nonzero to allow the mask to be added, zero to deny it. The mask and expiration time may be modified by the function, provided that the mask is not lengthened beyond the original string length.
void (*do_add_mask)(const User *u, uint8 type, MaskData *md)
Performs any extra actions necessary when a mask is added to the database, such as sending that mask to the network.
void (*do_del_mask)(const User *u, uint8 type, MaskData *md)
Performs any extra actions necessary when a mask is removed from the database.
int (*do_unknown_cmd)(const User *u, const char *cmd, char *mask)
Processes an unknown subcommand for the associated command, returning nonzero if the subcommand was handled, zero otherwise.

The function which implements the mask-related command support is do_maskdata_cmd(), defined below the database support functions in maskdata.c. This function behaves in the same way as a standard pseudoclient command handler, taking a single User * parameter giving the client that sent the command, and retrieving command parameters via strtok(NULL,...). After obtaining the subcommand name and its parameters (with double-quote processing for masks), the routine checks for the known subcommands ADD, DEL, CLEAR, LIST, VIEW, CHECK, and COUNT, processing each appropriately. If the subcommand is not one of these, it is passed to the command's do_unknown_cmd() function; if that function returns zero or does not exist, an error is sent to the client.

The subcommands themselves are implemented by local routines: do_maskdata_add(), do_maskdata_del(), and so on. These check the validity of the parameters for the particular subcommand and perform the appropriate action, using the language string indices in the MaskDataCmdInfo structure to send replies to the client that issued the command.

There are also three utility functions defined after the command handler routines:

void *new_maskdata()
Allocates, initializes, and returns a new MaskData structure. (The return value is void * to avoid a type warning in the database table declaration.)
void free_maskdata(void *record)
Frees the MaskData structure pointed to by record, Does nothing if record is NULL.
char *make_reason(const char *format, const MaskData *data)
Returns the reason string for the given mask and format string. If format contains a "%s", it will be replaced by the mask's reason string in the return value; otherwise, the returned string is identical to format. The result string is stored in a static buffer, which is overwritten by subsequent calls to make_reason().

With respect to the database management routines, MaskData records are stored in variable-length arrays, one array for each of the 256 possible mask types. (Arrays were chosen over hash tables for simplicity; since the most common use of these records is searching all records of a particular type for one that matches a given string, there would be little benefit to the use of a hash table.) The next and prev fields are not ordinarily required for array handling, but the code stores the (integer) array index value in the next field via a cast to void *, allowing the code to know where in the array a given record is located without having to search through the array every time (note that the num field is used for a different purpose—index numbers for session exceptions—and is not available). Aside from the standard database operations, which take a uint8 type parameter in addition to the mask record itself, the following functions are included:

MaskData *get_matching_maskdata(uint8 type, const char *str)
Searches for and returns (in the same manner as get_maskdata(), including expiration checks) a mask which matches the wildcard pattern given by str. If more than one mask matches, an arbitrary one is returned.
MaskData *get_exception_by_num(int num)
Retrieves a mask of the MD_EXCEPTION (session exception) type by its index number.
MaskData *move_exception(MaskData *except, int newnum)
Changes the index number of the given MD_EXCEPTION mask. (Internally, this reorders the mask array so that entries remain in order by index number.)
7-2-2-2. Autokills

As mentioned above, one of the main uses of this mask data code is the autokill module, operserv/akill, defined in akill.c and akill.h. While support for the OperServ AKILL and EXCLUDE commands simply makes use of the aforementioned do_maskdata_cmd() function, handling for the actual autokills and autokill exclusions themselves is defined within the autokill module. This includes:

One item of interest in the module setup code at the bottom of the source file is the handling of the EXCLUDE command. EXCLUDE can be enabled or disabled via a configuration file option (EnableExclude), for reasons discussed below. Rather than create two command tables, one with EXCLUDE and one without, the code simply modifies the EXCLUDE entry to have an empty command name if the command is disabled; since command names parsed by OperServ cannot be empty, the command will never be found. (In order to locate the entry again once the name has been cleared, a static variable is used, set during module initialization to point to the proper element of the command array. The name is restored during module cleanup so that a subsequent initialization will likewise be able to find it.)

The handling of autokill exclusions is trickier than it may seem at first glance, due to protocols which support network-wide autokill masks but not exclusion masks. If one takes the naive approach of simply adding autokill masks as usual, one will find that the effectiveness of the autokill exclusions is severely limited. For example, consider a network with an autokill for *@*.example.com and an exclusion for *@oper.example.com. As long as the only users in the example.com domain to connect are from the oper.example.com host, the exclusion will function as expected. However, as soon as a user from another example.com host connects, the autokill will be triggered and sent out to the network—with the unintended result that even users from oper.example.com are prevented from connecting, since Services has no way to inform other servers on the network about the autokill exclusion.

In order to avoid this problem, the operserv/akill module will not make use of a protocol's autokill features if that protocol does not also support autokill exclusions, and will simply send out a KILL message for each user to be disconnected from the network. However, this may not be desirable for networks that have no intention of using the autokill exclusion functionality. For this reason, the EnableExclude configuration option was added, allowing such networks to choose between taking advantage of the protocol's autokill feature and using exclusions with autokills.

7-2-2-3. S-lines

S-lines are the other primary user of the mask data code. These sets of client restriction masks (SGlines, SQlines, and SZlines) are implemented by the operserv/sline module, defined in sline.c and sline.h.

The S-line module is very similar to the autokill module, both functionally and internally (in fact, sline.c started out as a copy of akill.c). The primary difference is in the sharing of data and code between the three mask types. Since the command names differ only in a single character, the code takes the shortcut of using that character as the mask type for use with the MaskData support functions—this is the reason for the warning comment in maskdata.h about changing the type values—and including a %c token in relevant messages which is replaced by the mask type. This enables a single message, such as "%d masks on the S%cLINE list.", to be shared by code for all three sets of masks. Several functions are likewise shared by the three mask types, taking a type parameter to select which data set to operate on.

Naturally, each type of mask is interpreted differently, so the actual processing for each type is handled separately. SQlines, in particular, are checked by the utility routine check_sqline(), called from the callback functions do_user_check() and do_user_nickchange_after(), because there are several possible ways of handling a match:

7-2-3. Session limiting

The last method of client control, session limiting, is implemented by the operserv/sessions module, defined in sessions.c. The module consists of two major parts: the actual session maintenance and limiting code, and session exception mask management via the EXCEPTION command.

The session maintenance functionality is contained within the add_session() and del_session() routines, called from callback functions when clients join or leave the network, respectively. These routines keep track of each host with one or more clients on the network by means of a hash table containing Session structures, one per host. When add_session is called, it looks up the Session record for the new client's hostname, creating a new record with a count of zero if no record for that host already exists, then increments the host's client count by one; conversely, del_session() decrements the host's client count, deleting the Session structure when the count reaches zero.

The "limiting" part of session limiting is a check in add_session() to determine whether the host has "too many" clients on the network. Here, "too many" is defined by either the limit given in an exception record (see below) or the default limit given in the DefSessionLimit configuration directive; if adding the new client would cause the host's client count to exceed the relevant limit, then the client is killed (any clients already connected are left alone). An autokill can also be added for thte host depending on the configuration settings.

The module also provides a SESSION command to allow Services operators to view the contents of the session list; this can be used to find hosts from which a large number of clients are connecting. The SESSION command is available even if actual limiting of sessions is disabled (by setting DefSessionLimit to zero).

The second part of the module, session exceptions, allows fine-tuning of the default limit on client connections. For example, it may be desirable to allow extra connections from a particular host known to be used by IRC operators. Such exceptions to the default session limit are stored using MaskData structures with the MD_EXCEPTION type; each record is treated as a wildcard pattern against which a connecting client's hostname is matched, and if a match is found, that limit is used instead of the default, as described above. If more than one matching session exception exists, the first one in the list (from a user's point of view, the one with the lowest index number) is used.

Session exceptions are maintained using the EXCEPTION command. The processing for this command, in do_exception() and its subroutines, is very similar to other mask-type commands like AKILL; however, session exceptions require a limit value in addition to the mask, and it was considered simpler to keep the handling for EXCEPTION separate rather than modify do_maskdata_cmd() to handle both command formats. The EXCEPTION command also includes a MOVE subcommand, allowing one record to be moved relative to another within the list (so, for example, a newly-added exception can be moved earlier in the list to take precedence over other exceptions).

A careful look at the initialization code will show that the "user check" callback function, check_sessions() (which in turn calls add_session()), is added at a priority of -10. This is in order to ensure that the function is called after all other checks have been performed, as described in section 4-5-2 (and is a poor design choice for the reasons described there).

7-2-4. News

The last OperServ submodule is the operserv/news module, defined in news.c and news.h. This is a fairly simple module, containing routines to handle news item storage and implement the LOGONNEWS and OPERNEWS commands.

News items are stored in a single variable-length array of News structures, newslist. The News structure contains, aside from the text of the news item itself, the news type (NEWS_LOGON or NEWS_OPER), an index number used when deleting the item, the nickname of the client that added the item, and the time the item was added. As with MaskData structures, next and prev fields are included only to mirror other structures, and the array index is stored in the next field.

The two news commands, LOGONNEWS and OPERNEWS, share the same code, do_news(); this routine is called by the actual command handlers with one of the news type codes, either NEWS_LOGON or NEWS_OPER, and accesses an array of language string indices (msgarray[]) to return proper messages for each command, similar to do_maskdata_cmd(). Unlike most other commands (but like the OPER and ADMIN commands in the OperServ core), the news commands include a LIST command available to all IRC operators, and other subcommands (ADD and DEL) restricted to Services operators; thus, the command handler must perform privilege checks on its own rather than specifying a privilege check routine in the command table.


7-3. NickServ

NickServ is typically the first pseudoclient IRC users interact with. It was also the first pseudoclient created during Services' initial design, on which all other pseudoclients were based. The current NickServ is divided into a core module and four submodules implementing additional features; each module is described in its own section below.

7-3-1. NickServ core functionality

The core NickServ functionality is implemented by the nickserv/main module. Alongside the primary module source file, main.c, the module makes use of three additional source files: collide.c, handling the disconnection or forced removal of clients using unauthorized nicknames; set.c, implementing the SET command and its subcommands; and util.c, containing various utility routines used by NickServ.

NickServ makes use of two distinct header files. One, nickserv.h, defines the data structures used for storing nickname information (NickInfo and NickGroupInfo, described below) along with declarations of exported routines and macros used with nickname records. The other, ns-local.h, contains declarations of routines used within NickServ, necessarily declared extern because they reside in separate source files but not intended to be used outside the NickServ modules.

7-3-1-1. Nickname data structures and utility macros

Two separate structures are used to store nickname data; this is to facilitate the implementation of nickname links, as described in section 7-3-4. One structure, NickInfo, contains data that is distinct for each individual nickname; the other, NickGroupInfo, contains data that is shared among each group of linked nicknames.

The NickInfo structure includes the following data:

NickInfo *next, *prev
Used to link records together in the internal hash table.
int usecount
The record's usage count (number of gets minus number of puts). (Implementation note: As noted in section 7-1, this field currently serves no actual purpose.)
char nick[NICKMAX]
The actual nickname. Capitalization is as used when the nickname was registered, and does not change due to later actions. The buffer size, NICKMAX, is defined in the global header file defs.h.
int16 status
The nickname's status. This is a combination of zero or more of the following flags: Of these flags, NS_VERBOTEN and NS_NOEXPIRE are "permanent" flags (collected in the NS_PERMANENT mask), which are retained across restarts of Services, while NS_KILL_HELD and NS_GUESTED are "temporary" flags (collected in the NS_TEMPORARY mask), which are cleared each time the database is loaded from persistent storage. Note that the value 0x0001 (bit 0) is not used because it served a separate purpose in previous versions of Services.
char *last_usermask
The last user@host mask used by the owner of the nickname (i.e., a client authorized to use the nickname). If the owner is currently online, that client's user@host mask is used. On IRC networks where a "fake hostname" is available, that hostname is used instead of the client's actual hostname.
char *last_realmask
Like last_usermask, but uses the real hostname instead of any "fake hostname". On networks without such a fake hostname, this field is identical to last_usermask.
char *last_realname
The last "real name" string used by the owner of the nickname.
char *last_quit
The message used the last time the owner quit IRC. NULL if not available, such as for newly-registered nicknames.
time_t time_registered
The timestamp when the nickname was registered.
time_t last_seen
The timestamp at which the owner most recently used the nickname. Only updated when the owner stops using the nickname; if the nickname is currently in use, this field should not be relied on.
uint32 nickgroup
The ID of the nickname group with which this nickname is associated (see below).
uint32 id_stamp
The servicestamp of the client that last identified for the nickname. Used to retain identification status across Services restarts.
User *user
Not saved to persistent storage. A pointer to the User structure for the client currently using the nickname, or NULL if the nickname is not currently in used.
int16 authstat
Not saved to persistent storage. Zero or more of the following flags, indicating the nickname's authentication status:
int bad_passwords
Not saved to persistent storage. The number of consecutive failed password identification attempts for the nickname. Used to determine whether or not to kill a client for attempted password cracking.

The NickGroupInfo structure includes the following data:

NickGroupInfo *next, *prev
Used to link records together in the internal hash table.
int usecount
The record's usage count (number of gets minus number of puts). (Implementation note: As noted in section 7-1, this field currently serves no actual purpose.)
uint32 id
The nickname group's ID, a unique 32-bit value. Typically, this value is randomly assigned. The value zero is not allowed (it is used in NickInfo records to indicate the lack of an associated nickname group, as for forbidden nicknames).
nickname_t *nicks
uint16 nicks_count
Not saved to persistent storage. A variable-length array containing the nicknames associated with this nickname group. Used for convenience, to avoid having to search through the nickname database every time a list of nicknames is needed.
uint16 mainnick
The "main nickname" for the group, used to represent the nickname group in things such as channel access lists. Specified as an index into the nicks[] array.
Password pass
The password used for identification for the nickname group.
char *url
A URL associated with the nickname group. Can be arbitrarily set by the owner.
char *email
An E-mail address associated with the nickname group. Can be arbitrarily set by the owner, and may be required at registration time by setting the NSRequireEmail configuration option.
char *last_email
When mail-based authentication (see section 7-3-5) is in use, this field is set to the previous contents of the email field when the owner changes the E-mail address, and is cleared when the new address is authenticated; this allows the RESTOREMAIL command to function.
char *info
A free-form text string associated with the nickname group. Can be arbitrarily set by the owner.
int32 flags
A bitmask containing zero or more of the following nickname group flags: Note that the value 0x00000004 (bit 2) is not included in the above flags because it served a separate purpose in previous versions of Services. Instead, this value is used as a temporary flag (NF_NOGROUP) when loading databases from earlier versions of Services, to indicate that a nickname group does not yet have an ID value assigned.
int16 os_priv
The nickname group's privilege level with respect to OperServ. Can be any value, but typically either zero (no special privileges) or one of the following values: Other values are treated by the OperServ privilege checking code (see section 7-2-1) as having the next lowest recognized value.
int32 authcode
The authentication code set for the nickname group, or zero if no code is set. See section 7-3-5.
time_t authset
The timestamp when the nickname group's authentication code was set (meaningless if no code is set).
int16 authreason
The reason the nickname group's current authentication code was set (meaningless if no code is set). One of the following constants:
char suspend_who[NICKMAX]
The nickname of the client that suspended the nickname group (meaningless if the NF_SUSPENDED flag is not set).
char *suspend_reason
The reason the nickname group was suspended (meaningless if the NF_SUSPENDED flag is not set).
time_t suspend_time
The timestamp when the nickname group was suspended (meaningless if the NF_SUSPENDED flag is not set).
time_t suspend_expires
The timestamp at which the nickname group's suspension expires (meaningless if the NF_SUSPENDED flag is not set).
int16 language
The language preferred for messages sent to the nickname group. One of the LANG_* constants in the Services core's language.h.
int16 timezone
The time zone offset specified by the nickname group's owner for use in displaying times. A number of minutes (possibly negative) to be added to the UTC timestamps, or TIMEZONE_DEFAULT to use the Services process' default.
int16 channelmax
The maximum number of channels the nickname group is allowed to register, CHANMAX_UNLIMITED for no limit, or CHANMAX_DEFAULT for the default limit (set by the ChanServ CSMaxReg configuration setting).
char **access
int access_count
The nickname group's access list (see section 7-3-2).
char **ajoin
int ajoin_count
The nickname group's auto-join list (see section 7-3-3).
char **ignore
int ignore_count
The nickname group's memo ignore list (see section 7-5-2).
MemoInfo memos
The nickname group's stored memos (see section 7-5-1).
channame_t *channels
int channels_count
Not saved to persistent storage. The names of the channels currently registered by this nickname group.
User **id_users
int id_users_count
Not saved to persistent storage. Pointers to User structures for clients which have identified for this nickname group.
time_t last_sendauth
Not saved to persistent storage. The timestamp when the SENDAUTH command was last used for this nickname group (see section 7-3-5).
int bad_auths
Not saved to persistent storage. The number of times the AUTH command has been used with a bad authentication code for this nickname group (see section 7-3-5).

In addition, nickserv.h declares the following convenience functions and macros:

int nick_recognized(const NickInfo *ni)
int user_recognized(const User *u)
Returns whether the given client is recognized via an access list entry, regardless of whether the client has identified for the nickname group or not. The client can be specified by either NickInfo or User structure. (Nickname authorization flags are always cleared when a client disconnects from the network, so the NickInfo form will always return false if used on a nickname not currently in use.)
int nick_identified(const NickInfo *ni)
int user_identified(const User *u)
Returns whether the given client has identified for its nickname. The client can be specified by either NickInfo or User structure.
int nick_id_or_rec(const NickInfo *ni)
int user_id_or_rec(const User *u)
Returns whether the given client is recognized via access list or has identified for its nickname (or both). The client can be specified by either NickInfo or User structure.
int nick_ident_nomail(const NickInfo *ni)
int user_ident_nomail(const User *u)
Returns whether the NA_IDENT_NOMAIL flag is set for the given client; i.e., evaluates to true when has identified for its nickname but the nickname lacks a required E-mail address. The client can be specified by either NickInfo or User structure.
int ngi_unauthed(const NickGroupInfo *ngi)
Returns whether the given nickname group (more accurately, the nickname group's E-mail address) is unauthenticated. Note that use of the REAUTH command does not cause the nickname group to lose its authenticated status.
int valid_ngi(const NickGroupInfo *ngi)
Returns whether the given NickGroupInfo structure pointer is valid; i.e., evaluates to true when ngi is neither NULL nor NICKGROUPINFO_INVALID. (The latter value is used in User structures to indicate that the client has an associated NickInfo structure but no NickGroupInfo structure, as is the case for forbidden nicknames.)
const char *ngi_mainnick(const NickGroupInfo *ngi)
Returns the given nickname group's main nickname.
NickGroupInfo *get_ngi(const NickInfo *ni)
NickGroupInfo *get_ngi_id(uint32 id)
Retrieves the NickGroupInfo record corresponding to the given NickInfo or ID value; if there is no corresponding record, a warning message is logged and NULL is returned. (Like any other database "get" routine, the structure must be "put" with put_nickgroupinfo() when no longer needed.)
int check_ngi(const NickInfo *ni)
int check_ngi_id(uint32 id)
Returns whether there is a corresponding NickGroupInfo record for the given NickInfo or ID value, logging a warning message if not. (The put_xxx(get_xxx(...)) is a common way of checking for the existence of a record, since the put_xxx() explicitly allow a NULL parameter.)

The STANDALONE_NICKSERV define and (non-macro) utility functions are discussed in section 7-3-1-4.

7-3-1-2. Overall module structure

The overall structure of the NickServ module generally follows the same pattern as the OperServ module: variable and command declarations, database handling, PRIVMSG and other callbacks, and command routines.

The nickserv/main module uses two separate databases, one for NickInfo records and one for NickGroupInfo records. The database handling code is more or less straightforward, using hash.h to maintain the in-memory tables; however, since most of the record management routines take additional actions (for example, the "add" and "get" functions update the record's use count), the base hash functions are defined with a trailing underscore, like add_nickinfo_(), and the actual functions (like add_nickinfo()) are wrapped around these. NickServ exports all of the NickInfo and NickGroupInfo database functions, as well as a "put" function for each record type.

When saving the databases to persistent storage, the nicks[] array and mainnick field of NickGroupInfo records are not saved directly; rather, the main nickname itself is saved as a NICKMAX-sized buffer, and the nickname group is initialized with this nickname when first loaded (the array is subsequently filled in when the relevant NickInfo records are loaded).

NickServ keeps track of clients' status using callback functions for new clients, clients changing nickname, and disconnecting clients. The routines that do the actual processing, validate_user() (for a client starting to use a nickname) and cancel_user() (for a client no longer using a nickname), are located in util.c, discussed in section 7-3-1-4.

The NickServ commands themselves tend to be fairly complex, especially when compared to the OperServ command handlers. This is in part due to the wide range of features available in NickServ, and in part due to the fact that NickServ and its system of registered nicknames are the primary way by which clients authenticate themselves, and as such must handle a variety of circumstances to maintain security, while other pseudoclients simply rely on the authorization flags set by NickServ. The following command handlers are particularly worthy of note:

do_help()
NickServ's commands include a number whose help text changes based on factors such as the requesting client's IRC operator status, features available in the IRC protocol in use, and NickServ configuration settings. The code to handle help requests is accordingly complex, with many commands unable to rely on the help_cmd() routine.
do_register()
Just as NickServ is the gateway to the rest of Services' functions, the REGISTER command is the gateway to NickServ, providing one of the two methods by which a client can gain access to Services (the other being authentication to a previously registered nickname). The REGISTER handler must therefore be particularly careful to guard against abuse, both to prevent improper access to other Services commands and to prevent the REGISTER command itself from being abused by arbitrary clients. The command handler takes the following precautions before allowing a nickname to be registered:
do_dropemail()
do_dropemail_confirm()
The DROPEMAIL and DROPEMAIL-CONFIRM commands are the only two commands in Services that require state to be kept specifically for those commands. Due to the potential for data loss through an erroneous DROPEMAIL command, some form of confirmation was desired, such as an "Are you sure?" requester in response to a user deleting a file in a GUI. Since Services' interface is limited to single-line commands, however, this can only be accomplished through two commands, the second of which (DROPEMAIL-CONFIRM) serves to confirm the action requested by the first (DROPEMAIL). In order for this to be effective, the DROPEMAIL-CONFIRM handler must know which commands have been sent by whom, so that clients cannot send arbitrary DROPEMAIL-CONFIRM commands to get around the confirmation check. This is accomplished through the file-local dropemail_buffer[] array, which holds the most recently issued, unconfirmed DROPEMAIL commands. (A single state record stored in the User structure was another possibility, but one that was discarded to avoid bloat in that structure, particularly since the vast majority of clients would never use the command anyway.) When a valid DROPEMAIL command is given, the client is told the number of nicknames that would be deleted, and the given mask is stored in the buffer array, with the oldest unconfirmed mask removed if no slots are empty. A DROPEMAIL-CONFIRM for the same mask will then locate the appropriate buffer slot, ensure that the same client sent both commands and that the elapsed time between the two commands is not too long (as defined by the NSDropEmailExpire option), and performs the actual nickname deletion.
do_info()
The INFO command has the ability to show extended information about a nickname with the option ALL (only available to the nickname owner or Services administrators). However, not all nicknames have any additional information to be displayed. To prevent the "use ALL for more information" message from being appended if there is not actually anything else to show, the INFO command handler uses a method inspired by super-user privilege checks in the Linux kernel, which keeps track of whether a process has taken advantage of those privileges. When the macro CHECK_SHOW_ALL is included in a conditional test, it will evaluate to true when the ALL option is present (and the client has permission to use it), but a separate flag variable, used_all, will also be set regardless of the presence of ALL; the routine can then determine whether there were any items that would have been displayed if ALL was given. As noted in the source code comments, the macro should be the last test in any conditional expression which uses it, to prevent used_all from being set for an item that will not actually be displayed due to a subsequent test.
do_set()
do_unset()
As the SET and UNSET commands (SET in particular) have a large number of options, they are defined in a separate source file, set.c. See section 7-3-1-3 for details.

NickServ also includes a debug command enabled by the DEBUG_COMMANDS preprocessor symbol: LISTNICK, which displays the NickInfo and (if present) NickGroupInfo data for a given nickname.

In addition to the ordinary module setup code, the nickserv/main module supports two command-line options. One, -encrypt-all, is recognized by the Services core; NickServ's init_module() routine checks the corresponding global flag, encrypt_all and, if it is set, encrypts all nicknames using the encryption type specified by the core's EncryptionType setting. The other option, -clear-nick-email, is NickServ-specific, and is handled by do_command_line(), a callback function for the core's "command line" callback; when the option is encountered, the callback function clears the E-mail address from all nickname groups.

As several messages used by NickServ can change based on configuration options or the features available in the IRC server, the initialization and cleanup code (as well as the reconfiguration handler, do_reconfigure()) call mapstring() to adjust the messages appropriately. The commands REGISTER, DROPEMAIL/DROPEMAIL-CONFIRM, and GETPASS can also be disabled by configuration options; the commands are disabled by setting the name field of the corresponding Command structure to the empty string, so that it will not be found when the command table is searched. Pointers to the structures are saved in file-local variables so that the names can be restored at reconfiguration or module cleanup time.

7-3-1-3. The SET and UNSET commands

The handlers for the SET and UNSET commands, do_set() and do_unset() in set.c, work much like miniature versions of the top-level NickServ message handler nickserv(), in that they check which option name was used with the command and call an appropriate subroutine to do the actual work. However, the do_set() routine parses the option parameters itself, rather than leave such parsing to the individual routines (UNSET does not take any additional parameters, so no such parsing is needed); for this reason, the setup code in do_set() is more complex than that in nickserv(), since the INFO option treats the entire line as a single parameter, HIDE takes two single-word parameters separated by a space, and the other options take a single one-word parameter.

Additionally, both SET and UNSET can be used by Services administrators to set options for other users' nicknames. For this reason, the individual option-setting routines take both a User * and a NickInfo * parameter, where the NickInfo * parameter is the nickname whose options are to be changed (if the client giving the command is not a Services administrator or does not give a target nickname, this will simply be equal to the ni field of the User structure). Implementation note: This raises an interesting problem—how does an option's handler routine tell the difference between SET option, SET !MyNick option, and SET !OtherNick option when sending result messages? The simple answer is that it doesn't: all option handlers use the "your nick" message style, as mentioned in Appendix D of the user's manual. If implemented, it would probably be reasonable to ignore the distinction between the SET option and SET !MyNick option cases, and simply judge which message to use by comparing the NickInfo parameter with the ni field of the client's User structure.

The option handlers themselves are simple for the most part, checking the option value given and setting or clearing the relevant flag or field in the NickInfo structure or its associated NickGroupInfo structure. Routines which deserve special mention are:

do_set_password()
The password setting itself is straightforward (note that the memory containing the cleartext password and the temporary copy of the encrypted password is cleared as soon as it is no longer needed); however, the routine first checks the NSSecureAdmins option, and disallows the change if the target is a (different) Services administrator and the command sender is not the Services super-user.
do_set_email()
This routine makes several checks mostly related to mail authentication before allowing the E-mail address to be changed: If the above checks all pass, the change is performed, and if the client used to have the NA_IDENT_NOMAIL status and an E-mail address was set, the status is changed to NA_IDENTIFIED. The routine also features its own callback, "SET EMAIL", used by the mail authentication code. Note that this routine does not check the NSRequireEmail configuration option, and assumes that if it is passed a NULL value, indicating that the address should be unset, then that is valid. (In fact, do_unset() checks NSRequireEmail before calling do_set_email().)
do_set_timezone()
SET TIMEZONE allows the time zone to be specified as either a literal time offset (-5, +6:30, etc.) or a time zone name. Time zone names (other than "GMT+/-n" and "UTC+/-n", which are treated as literal offsets) are parsed using the timezones[] table defined just above the do_set_timezone() routine itself, which (hopefully) includes most common time zones; the table can of course be modified to include other time zones as particular networks desire. Once the the time zone has been set, a message is sent to the calling client giving the current time in the resulting time zone; however, this is tricky if the calling client is a Services administrator changing the setting for another nickname, because strftime_lang() always uses the time zone setting of the nickname used to select the language. To get around this, the routine determines the difference between the calling nickname's time zone and the target nickname's time zone, adjusting the timestamp passed to strftime_lang() by that amount (multiplied by 60, since the timezone field is specified in minutes). Incidentally, support for "daylight saving time" as used in some countries was deliberately omitted, partly due to the difficulty of supporting the various systems used in different countries, and partly because the details of such systems are highly dependent upon each country's political landscape and can change at any time (witness the abrupt extension to DST proposed, and eventually implemented, in the United States of America in 2006).
7-3-1-4. NickServ utility routines

Most of the utility routines used by NickServ are collected in the file util.c. This file has two functions: aside from providing utility functions to NickServ itself (several of which are exported for use by other modules), it can also be #include'd in an external source file to provide definitions of the four routines new_nickinfo(), free_nickinfo(), new_nickgroupinfo(), and free_nickgroupinfo(), so that such files do not have to define similar routines themselves. This latter mode is activated by defining the STANDALONE_NICKSERV preprocessor symbol, as documented in the comments at the top of util.c. In this case, only the four routines mentioned above are defined, with the rest of the file commented out using #ifndef; additionally, the new_nickgroupinfo() routine does not check for the presence of the nickname group IDs it generates, as it cannot assume that get_nickgroupinfo() is available.

With respect to its primary use as part of NickServ, util.c defines the following routines:

NickInfo *new_nickinfo()
Returns a pointer to a newly-allocated and initialized NickInfo structure. (For creating a new record in the database, makenick() is preferred; see below.)
void free_nickinfo(NickInfo *ni)
Frees the given NickInfo structure and all associated data. (See delnick() below for removing a nickname record from the database.)
NickGroupInfo *new_nickgroupinfo(const char *seed)
Returns a pointer to a newly-allocated and initialized NickGroupInfo structure. If seed is not NULL, then it is used to generate an initial ID value for the nickname group; if that ID value is used, new values are randomly generated until an unused one is found. (If the code loops NEWNICKGROUP_TRIES times without finding an unused value, an error is returned; assuming a good random number generator, the default value of 1000 should ensure success on typical databases. NEWNICKGROUP_TRIES is defined in ns-local.h.) If seed is NULL, then the new nickname group's ID is left at zero.
void free_nickgroupinfo(NickGroupInfo *ngi)
Frees the given NickGroupInfo structure and all associated data. (See delgroup() below for removing a nickname group and all its nicknames from the database.)
NickGroupInfo *_get_ngi(NickInfo *ni, const char *file, int line)
NickGroupInfo *_get_ngi_id(uint32 id, const char *file, int line)
Implement the get_ngi() and get_ngi_id() macros, respectively. file and line are the source file and line from which the function was called, and are filled in by the corresponding macro with __FILE__ and __LINE__.
int has_identified_nick(const User *u, uint32 group)
Returns whether the given client has identified for the nickname group indicated by group.
int reglink_check(User *u, const char *nick, char *password, char *email)
Calls the "REGISTER/LINK check" callback and returns its result. (A utility function is used rather than directly calling call_callback_4() because the nickserv/link module needs to make use of the callback as well, and the module system does not allow one module to call another's callbacks (which would be bad design in any case).
void update_userinfo(const User *u)
Updates the user information for the client's nickname. The NickInfo fields last_usermask, last_realmask, and last_realname are set from the corresponding fields of the User structure, and the last_seen field is set to the current time. u->ni is assumed to be non-NULL.
int validate_user(User *u)
Sets the ni and ngi fields of the User structure to point to the NickInfo and associated NickGroupInfo, if any, for the client's nickname (if an error occurs looking up the nickname group, u->ni is set to NULL and u->ngi is set to NICKGROUP_INVALID); then compares the client's information with the nickname data and determines what level of access for the nickname should be granted to the client. Returns 1 if the client is granted either NA_IDENTIFIED or NA_RECOGNIZED access, otherwise zero.

This routine, along with the REGISTER command handler, is one of the two "points of entry" into Services, and as such is a critical point for Services security. This is particularly relevant for the section of code conditionally granting full (NA_IDENTIFIED) nickname access; while has_identified_nick(), mentioned above, operates purely on data which has been seen since Services started (specifically, the list of nicknames the client is known to have become identified for, maintained by set_identified()) and is comparatively safe, the second check, which matches the servicestamp, username, and hostname of the last client to identify with those of the current client, needs special attention to ensure that it does not allow clients to gain improper access. As noted in the comments in that section of code, the servicestamp provides a fairly high level of protection on servers which support it natively, while that level is reduced for servers which do not (such servers are rare nowadays). The entire section of code can be disabled with the NoSplitRecovery configuration option for added security.

If the client is determined not to have identified for the nickname previously, the routine continues, determining whether to give NA_RECOGNIZED access. "Recognized" status is only implemented by access lists (see section 7-3-2), and if the corresponding module is not loaded, the NA_RECOGNIZED flag will never be set on any nickname, except when set along with NA_IDENTIFIED. Likewise, if a nickname's NF_SECURE flag is set, then NA_RECOGNIZED will not be set (and zero will be returned from the routine) even if the client is in fact recognized.

If the client is not identified or recognized for the nickname, validate_user() checks whether the client should be killed or nick-changed, setting an appropriate timeout or calling the collide routines (see section 7-3-1-5 below) depending on the nickname group settings. However, if the client was recognized (which will only be true if the nickname has the SECURE option set and thus NA_RECOGNIZED was not set), the kill checks are not performed, allowing the client to identify at its leisure.

Finally, the routine checks the nickname's expiration time, and if it is due to expire "soon" (as defined by the NSExpireWarning configuration option), a warning notice is sent to the client.

void cancel_user(User *u)
Updates a client's nickname data when the client stops using the nickname. The last_seen field is updated if the client was either identified or recognized; the authstat field is cleared along with temporary status flags in the status field; an enforcer is introduced if the client was killed or nick-changed (see section 7-3-1-5); the cancel user" callback is called; and any active nick collide timeouts are removed. The ni and ngi fields of the client's User structure are also reset to NULL.
void set_identified(User *u)
Marks the given client as having identified for the nickname it is currently using. In addition to setting the authentication status to NA_IDENTIFIED | NA_RECOGNIZED and updating the nickname's id_stamp field, the routine adds the nickname group ID to the list of nickname groups for which the client has identified, stored in the User structure and checked by has_identified_nick().
NickInfo *makenick(const char *nick, NickGroupInfo **nickgroup_ret)
Creates a new NickInfo record with the given nickname, adds it to the database, and returns a pointer to the new record. If nickgroup_ret is not NULL, then a new NickGroupInfo record is also created for the nickname, the nickname's nickgroup field is set accordingly, and a pointer to the NickGroupInfo record (which is also added to the database) is stored in the variable pointed to by nickgroup_ret. Returns NULL on error.
int delnick(NickInfo *ni)
Removes the given nickname from the database and frees all resources used by the NickInfo structure. If the nickname was the last of its group, then the nickname group is deleted as well. Returns nonzero on success, zero on error.
int delgroup(NickGroupInfo *ngi)
Removes the given nickname group from the database, along with all associated nicknames. Returns nonzero on success, zero on error.
int drop_nickgroup(NickGroupInfo *ngi, const User *u, const char *dropemail)
Removes the given nickname group from the database, like delgroup(), but first log information about the nicknames to be deleted. u is the User structure for the client that sent the command resulting in the deletion. dropemail should be:
void suspend_nick(NickGroupInfo *ngi, const char *reason, const char *who, const time_t expires)
Suspends the given nickname group, copying the parameters reason, who, and expires into the suspension data fields. (If expires is zero, then the suspension will not expire.)
void unsuspend_nick(NickGroupInfo *ngi, int set_time)
Cancels the suspension on the given nickname group. If set_time is nonzero, the last-seen time of each nickname in the group will be updated according to NSSuspendGrace to prevent the nickname from expiring for that length of time (if NSSuspendGrace or NSExpire are not set, or if the nickname already has enough time before expiration, the last-seen time will not be changed).
int nick_check_password(User *u, NickInfo *ni, const char *password, const char *command, int failure_msg)
Performs a password check for a nickname as part of a NickServ command. If the password is incorrect or an error occurs when checking, a notice will be sent to the client; a WALLOPS will also be sent for repeated bad password attempts on the same nickname. u is the User structure for the client that issued the command; ni is the NickInfo structure for the nickname whose password is being checked; password is the password given by the client, command is the name of the command being executed; and failure_msg is the index of the message (language string) to be sent if an error occurs when checking the password.
int count_nicks_with_email(const char *email)
Counts and returns the number of registered nicknames with the given E-mail address. If a nickname has the given address but it is awaiting mail authentication, the value returned is negative; for example, if there are five nicknames using a given address but the address is not authenticated, -5 would be returned. Note that this function must scan through the entire nickname database, so care should be taken not to call it too frequently.

util.c also defines initialization and cleanup routines, init_util() and exit_util(), which take care of registering and unregistering the callbacks used by various utility functions. The routines are called as part of the nickserv/main module iniitialization and cleanup.

7-3-1-5. Nickname colliding

In the general sense, a "nickname collision" is what happens when a client on an IRC network attempts to use a nickname that is already in use by another client. The original (RFC 1459) solution to this was to kill both clients, but with the advent of timestamps, most modern servers only kill one or the other depending on the timestamps of the two colliding clients. Early versions of Services took advantage of this behavior to implement kill protection: by introducing an "enforcer" pseudoclient with an appropriate timestamp, the old client would be killed and would not be able to reconnect with the same nickname.

With respect to Services, then, "nickname colliding" is the act of forcing a client to stop using a particular nickname. While the nickname collision method itself has been abandoned, both to avoid depending on particular collision semantics and to provide a more meaningful disconnect message to the client ("Nick kill enforced" rather than an arbitrary server collision message), and although many modern IRC servers allow Services to forcibly change a client's nickname without going as far as disconnecting the client altogether, the term "colliding" is still used to refer to this set of actions.

Nickname colliding functionality is provided by the file collide.c. This file provides two methods of colliding nicknames: directly, or via timeouts. The nickname colliding code also has its own initialization and cleanup functions (init_collide() and exit_collide()), which are called from the nickserv/main module initialization and cleanup routines.

The following routines are used when colliding nicknames directly:

void collide_nick(NickInfo *ni, int from_timeout)
Collides the given nickname, either killing the client using the nickname or forcibly changing the client's nickname to a "guest" nickname depending on configuration settings. from_timeout is used internally with collide timeouts, and should always be zero when called externally. This routine automatically calls introduce_enforcer() after the client has been killed or nick-changed (in the case of a forced nickname change, the enforcer is introduced by cancel_user() upon receipt of the NICK message indicating that the client's nickname has been changed). Any pending nickname collide or "433" timeouts (see below) on the nickname are cancelled by thie routine.
void introduce_enforcer(NickInfo *ni)
Introduces an enforcer pseudoclient for the given nickname, to prevent other clients from using the nickname. This routine automatically adds a timeout to call release_nick() after NSReleaseTimeout seconds have passed.
void release_nick(NickInfo *ni, int from_timeout)
Removes the enforcer pseudoclient for the given nickname, allowing other clients to use it again. As with collide_nick(), from_timeout is used internally with collide timeouts, and should always be zero when called externally. Any pending release timeouts on the nickname are cancelled by this routine.

Callers can also establish timeouts to collide or release a nick after a certain time. To avoid each caller having to include its own timeout handlers, collide.c provides two wrapper routines around the generic timeout functions:

void add_ns_timeout(NickInfo *ni, int type, time_t delay)
Adds a timeout of the given type on the given nickname to occur in delay seconds. type can be any of the following:
void rem_ns_timeout(NickInfo *ni, int type, int del_to)
Removes any timeout of the given type on the given nickname; ni can be NULL to cause timeouts on all nicknames (of the given type) to be removed. type can be any of the type values used with add_ns_timeout(), or -1 to remove timeouts of all types. del_to should always be nonzero when called externally (the parameter is used for calling from within a timeout function, where it is not necessary to delete the Timeout structure as well).

Each of the three timeout types has its own timeout function: timeout_collide(), timeout_release(), and timeout_send_433(). The timeout functions check for any change in status (such as identification for the nickname) before performing their respective functions.

7-3-2. Nickname access lists

Nickname access lists are managed by the nickserv/access module, defined in access.c. The module is quite simple, consisting of a database table, two callback functions, and a NickServ command (ACCESS). For simplicity, the module (along with other NickServ submodules) assumes the presence of the nickserv/main module rather than explicitly importing every required NickServ symbol, although the module's initialization does look up the nickserv/main module handle for use in adding the requisite callbacks and the ACCESS command.

One unusual feature is the use of a static buffer for reading and writing database records. Since the access list itself is stored in the corresponding nickname group's NickGroupInfo structure as an array, the entries must be extracted and made available to the database subsystem in an independent (normalized) format. This is done by using a {nickgroup-ID,access-mask} record format, and providing a single record buffer. When loading data from persistent storage, the table's newrec() function returns a pointer to this buffer, taking advantage of the fact that only one record is loaded at a time (see section 6-2-1); the insert() routine then looks up the nickname group stored in the buffer and appends the given access mask to that nickname group's access list, while the freerec() routine frees the access mask string. When saving data, the first() and next() routines call first_nickgroupinfo() and next_nickgroupinfo() in turn, looping through each access mask of each nickname group.

Ideally, the access lists would be stored in memory in the same fashion, as a distinct table using the nickname group ID as a key. However, since the current database implementation does not provide an efficient way to look up records matching arbitrary criteria (like a SELECT statement in SQL), and since problems would ensue when trying to save the data using the (deprecated, but still available) database/version4 module, the in-memory data structures were left as is. See the comments in the autojoin.c source file for a more complete description.

Other than this, there is little of note in the module; the do_access() command handler simply adds or removes the requested mask to or from the access list, and the callback functions take care of setting an initial access mask on registration and determining whether the user should be treated as recognized by validate_user().

7-3-3. Nickname auto-join lists

Nickname auto-join lists are managed by the nickserv/autojoin module, defined in autojoin.c. Aside from the details of its operation, this module is nearly identical to the nickserv/access module, including the hack used for database loading and saving; see the discussion of that module above (section 7-3-2) for details.

7-3-4. Linking and nickname groups

The distinction between "nicknames" and "nickname groups" has been made several times above. Ordinarily, this is only of importance as far as which structure is accessed (NickInfo or NickGroupInfo); however, the nickserv/link module, defined in link.c, allows nicknames to be linked together by assigning the same nickname group to both nicknames. This results in all information in the NickGroupInfo structure being shared among all linked nicknames, with only the data in the NickInfo structure kept separately for each nickname.

The nickserv/link includes three commands: LINK, UNLINK, and LISTLINKS, as well as one additional SET option, SET MAINNICK (linked in through NickServ's "SET" callback). Of these, LISTLINKS and SET MAINNICK are straightforward: do_listlinks() simply echoes the contents of the nicks[] array for the calling client's nickname group (or the specified nickname's group for Services operators), and do_set_mainnick() modifies the nickname group's mainnick field based on the given nickname.

When the LINK command is given to link a new nickname to the caller's nickname group, do_link() first ensures that the new nickname is not already in use and that creating the link would not cause the caller's total number of nicknames to exceed the NSRegEmailMax limit, if set. (Note that LINK does not abort if the mail address is not authenticated, simply checking the absolute value of the return from count_nicks_with_email() against the limit; creating the link does not in itself grant any additional privileges to the user, and can at most be used to "hide" from other users while maintaining current privileges.) If these checks pass, a new NickInfo structure is created, passing NULL as the nickgroup_ret parameter to makenick() to indicate that a new nickname group is not required; updates the NickInfo structure with the calling user's data; stores the nickname group ID in the new nickname's nickgroup field; and appends the new nickname to the nickname group's nicks[] array.

UNLINK acts similarly to the Services administrator command DROPNICK for a single nickname in a group. However, since UNLINK is not limited to Services administrators, care must be taken that an unprivileged client is not allowed to delete nicknames from other nickname groups; this check is made by ensuring that (1) the target nickname has a valid associated NickGroupInfo structure and (2) the nickname group IDs of the two nickname groups are equal, and disallowing the command otherwise if the FORCE option is not given. (The check is made on the FORCE option, disallowed for unprivileged clients, rather than on the client's privilege level in order to prevent accidental deletion of others' nicknames by Services administrators.) If the command is allowed, the routine then deletes the nickname by calling delnick(); if the nickname group's main nickname is the one being unlinked, delnick() automatically adjusts the mainnick field to the next nickname in the nicks[] array (or the previous nickname, if the deleted nickname was the last one in the array).

7-3-5. E-mail address authentication

While it has long been possible to associate an E-mail address with a registered nickname, there was traditionally no way to ensure that the address given was in fact a valid one belonging to the nickname owner. Since Services 5.0, such functionality has been provided by the nickserv/mail-auth module, defined in mail-auth.c. As described in the user's manual, E-mail address authentication works by assigning a random "authentication code" to the nickname, then sending an E-mail message to the registered address containing that code; the owner is not allowed to identify to the nickname until the code has been entered, ensuring that the address is one which the owner has (or at least had, at the time the message was sent) access to.

Internally, this processing is managed with the authcode, authset, and authreason fields of the NickGroupInfo structure. When an event occurs requiring E-mail address authentication, the module generates a random 9-digit numeric code (100000000 through 999999999 inclusive—codes with leading zeroes are not used in order to avoid confusion), stores the code in the nickname group's authcode field, and calls the mail subsystem's sendmail() routine to send a message to the nickname group's registered E-mail address (see section 8-3 for information about how mail is sent). Additionally, the current timestamp is stored in the authset field, and the reason for setting the code (one of the NICKAUTH_* constants) is stored in the authreason field. (In early versions of the module, the reason was stored in two bits of the authentication code; this method was later rejected, however, as being too kludgey and inflexible as well as leaking information.) The presence of a nonzero value in the authcode field indicates that the nickname group is awaiting authentication, and a nickname identification callback function ensures that clients are not allowed to identify for such nicknames (nor does validate_user() allow automatic identification if an authentication code is present), thus effectively preventing their use. By issuing an AUTH command with the correct code, a nickname owner can clear the nickname group's authcode field, allowing identification to the nickname(s) once more.

The module begins with several utility functions:

void make_auth(NickGroupInfo *ngi, int16 reason)
Generates a random authentication code, and sets the nickname group's authcode, authset, and authreason fields appropriately. The reason parameter is copied directly into the authreason field, without checks on its value.
void clear_auth(NickGroupInfo *ngi)
Clears the given nickname group's authentication code (if any), as well as all related nickname group data fields (including the previous E-mail address).
int send_auth(User *u, NickGroupInfo *ngi, const char *nick, int what)
Sends an E-mail to the given nickname group's owner containing the nickname's current authentication code. u is the User structure for the client whose command caused the message to be sent; nick is the specific nickname for which the command was issued; and what is one of the IS_* constants defined for the routine (internally a language string index for the mail body or -1 for the special case of SETAUTH). The routine itself is defined as send_auth_, taking a line parameter indicating where in the source the routine was called from; this is filled in automatically by the send_auth() macro. In order to inform the calling client of the success or failure of sending the message, send_auth() creates a sendauth_data structure for each message sent, used in the two routines listed below.
void send_auth_callback(int status, void *data)
The callback function used for sendmail() when called from send_auth(). This routine uses the sendauth_data structure for the sent message (passed as the data parameter) to send a reply to the client that issued the original command, and also to clear the nickname group's last_sendauth field if the command used was SENDAUTH and the message could not be sent. The structure is then removed from the global list and freed.
int sendauth_userdel(User *user, const char *reason, int is_kill)
Used as a callback function for the core's "user delete" callback. Iterates through the sendauth_data list, clearing the User pointer of any entries for the user being removed; this causes send_auth_callback() to skip sending a reply when the mail sending completes.

These routines are followed by the command handlers for the commands supported by the module: AUTH, SENDAUTH, REAUTH, RESTOREMAIL, and the Services administrator commands SETAUTH, GETAUTH, and CLEARAUTH. Of these, the only fairly complex one is the AUTH handler, do_auth(), as it must watch for attempts to guess the authentication code. (Invalid AUTH commands are treated the same as bad passwords to IDENTIFY or other commands, by calling bad_password() and incrementing the nickname group's bad_auths field, which itself can generate a warning via WALLOPS.)

Finally, the module includes four callback functions. Two of these, do_registered() and do_set_email(), hook into the NickServ callbacks for the REGISTER and SET EMAIL commands, respectively, dropping the client's identified status and generating and sending an authentication code. (Changes to the E-mail address made by Services administrators are not subject to authentication, however.) These are followed by the IDENTIFY command callback function do_identify(), which disallows IDENTIFY for nicknames with pending authentication codes, and the expiration check callback function do_check_expire(), which drops a newly-registered nickname after NSNoAuthExpire seconds (if set) if not authenticated, and also clears any pending REAUTH after the same amount of time (the user can subsequently use a second REAUTH if necessary).

Since the ability to send E-mail is essential for this module to work, the init_module() function checks for the presence of the mail/main module, refusing to load if it is not available.


7-4. ChanServ

ChanServ is the most complex of the standard Services pseudoclients, owing to the variety of operations that can be performed on channels. There is no easy way to split those operations up into separate modules (except by individual command, an avenue which has not been pursued), and as a result, the core ChanServ module is itself the largest module in Services.

As with NickServ, the core ChanServ module, chanserv/main, is split up over several source files. These are discussed in section 7-4-1 and its subsections, with the exception of the access.c source file, which is discussed in section 7-4-2 along with the access list manipulation submodules, chanserv/access-levels and chanserv/access-xop.

7-4-1. ChanServ core functionality

The core ChanServ module, chanserv/main, is built from several source files, along much the same lines as the core NickServ module: main.c, containing the central module code and most command handlers; access.c, for handling channel access lists; check.c, for checking the status of a channel against registered data and making appropriate changes; set.c, implementing the SET command; and util.c, defining various ChanServ utility functions.

7-4-1-1. Channel data structures

As with NickServ, ChanServ splits its declarations into two header files: chanserv.h, containing structures and routine declarations intended to be exported to other modules, and cs-local.h, for internal use by ChanServ only. (There is also a separate header file, access.h, specifically for channel access list definitions; this is discussed in section 7-4-2-1.)

The channel data structure, ChannelInfo, is naturally exported. As ChanServ does not have the concept of "links" or "channel groups" that NickServ uses with nicknames, all data for a channel is stored in this single structure. ChannelInfo does, however, include three substructures, defined before it in chanserv.h:

ChanAccess
Contains the data for an entry on a channel's access list. Access list entries are stored by nickname group ID, rather than by nickname, both to optimize checking an access list for a particular client and to eliminate the possibility of two or more entries matching a single client. (This is why only registered nicknames are allowed on channel access lists, and why channel access list entries are always displayed using the nickname group's main nickname, regardless of the nickname actually added to the list.) Rather than resizing the array every time a change is made to the list, deleted entries are left in memory with a nickname group ID of zero, and subsequent adds reuse these entries before attempting to expand the array. The channel field is used to link the record with its associated channel when loading and saving data. There are also a number of access-level related constants declared below this structure, such as the maximum and minimum access levels and the equivalent access levels for the XOP commands.
AutoKick
Contains the data for an entry on a channel's autokick list. As with access list entries, deleted entries are left in the list rather than resizing the array (a NULL value for the mask string indicates an unused entry), and the channel field is used when loading and saving data to link the record with its associated channel.
ModeLock
Contains a channel's mode lock data, including the set of modes locked on and off along with parameters for modes that require them; with the exception of the presence of two mode sets rather than one (modes can be locked on, locked off, or neither), the structure's contents are the same as the fields used in channel data structures (Channel records) to record the channel's current mode. The mode sets are normally maintained as bitmasks, but when used in the convert-db tool, they are defined as strings instead to allow lossless conversion to XML without needing to know the specific set of modes supported by the program that created the database. Unlike the ChanAccess and AutoKick structures, there is only one ModeLock structure per channel, and the data in the structure is saved along with the channel record itself, so there is no need for a separate channel field.

chanserv.h also defines constants for each of the channel privilege levels (indices into the levels[] array of the ChannelInfo structure). As noted in the comment above the list of definitions, changing the values of any of the constants will cause malfunctions when using the database/version4 module, since that module simply reads in the list of levels as a block. (This is why the index 18, formerly used for CA_AUTOOWNER, is unused—to avoid problems with databases in which that index is used.)

The ChannelInfo structure itself contains the following fields:

ChannelInfo *next, *prev
Used to link records together in the internal hash table.
int usecount
The record's usage count (number of gets minus number of puts). (Implementation note: As noted in section 7-1, this field currently serves no actual purpose.)
char name[CHANMAX]
The channel's name. Capitalization is as used when the channel was registered, and does not change due to later actions. The buffer size, CHANMAX, is defined in the global header file defs.h.
uint32 founder
uint32 successor
The nickname group ID of the channel's founder and successor, respectively. If the channel does not have a successor set, the successor field will be zero.
Password founderpass
The founder password for the channel.
char *desc
The channel's description, as specified at registration time or with a later SET DESC command.
char *url
The URL associated with the channel, as set with the SET URL command. NULL if no URL has been set.
char *email
The E-mail address associated with the channel, as set with the SET EMAIL command. NULL if no E-mail address has been set.
char *entry_message
The channel's entry message (the message sent as a NOTICE to clients entering the channel), as set with the SET ENTRYMSG command. NULL if no entry message has been set.
time_t time_registered
The timestamp at which the channel was registered.
time_t last_used
The timestamp at which the channel was last used (see section 3-2-3 of the user's manual for details of how the last-used time is set).
char *last_topic
char last_topic_setter[NICKMAX]
time_t last_topic_time
The topic most recently set on the channel, along with the nickname of the client that set the topic and the timestamp at which it was set. If no topic has been set on the channel, last_topic will be NULL, and the other two fields will be undefined.
int32 flags
A bitmask containing zero or more of the following channel flags: The flag values 0x00000100 and 0x00000400 are unused to avoid difficulties with databases from earlier versions of Services which used these values.
char suspend_who[NICKMAX]
char *suspend_reason
time_t suspend_time
time_t suspend_expires
Suspension data for the channel, if the CF_SUSPENDED flag is set. Used in the same fashion as the same-named fields in the NickGroupInfo structure (see section 7-3-1-1).
int16 levels[CA_SIZE]
The channel access levels corresponding to each of the channel privileges (CA_INVITE, CA_AKICK, and so on). A value of ACCLEV_DEFAULT indicates that the corresponding privilege should use the default access level defined in access.c; a value of ACCLEV_INVALID indicates that the corresponding privilege is disabled entirely, except with respect to the channel founder.
ChanAccess *access
int16 access_count
A variable-length array containing the channel's access list.
AutoKick *akick
int16 akick_count
A variable-length array containing the channel's autokick list.
ModeLock mlock
The channel's mode lock data.
Channel *c
Not saved to persistent storage. Points to the Channel structure for the channel if it is currently in use.
int bad_passwords
Not saved to persistent storage. The number of times an incorrect password has been given for a channel command (such as IDENTIFY) since the last correct password. Used to warn about attempts to crack the password.

The cs-local.h header file includes three definitions used internally by ChanServ:

MAX_MLOCK_PARAMS (constant)
Sets the maximum number of command parameters that the SET MLOCK command will process. This constant is used to avoid the overhead of dynamically allocating an argument array for every invocation of the command; any parameters passed beyond this limit will be silently ignored. The default value, 256, is more parameters than are can be passed in an RFC-standard 512-byte line (even if every parameter is one character, subtracting out the "SET MLOCK" leaves space for only 251 parameters, not considering the trailing CR/LF and other IRC protocol overhead).
ChanOpt (structure)
Used to define channel option data, for use by the SET and INFO commands. The structure has the following fields:
RET_* (constants)
Return values from access list modification routines. See section 7-4-2-1 for details.

One other point of note in cs-local.h is the renaming of several functions in set.c and util.c using #define directives, such as renaming init_set() to init_set_cs(). This is to avoid conflicts with NickServ, which includes functions of the same name in its own set.c and util.c. While the functions involved are not used outside of ChanServ, they must be declared external for the multi-file link to succeed; as a result, if the functions are not renamed, linking the final program when using static modules will result in a symbol name clash when the symbols from both modules are processed. This problem does not occur when using dynamic modules, since the presence of conflicting symbols in dynamic modules is not itself an error, and no attempt is made to reference either set of symbols from any other module (the references within the respective modules are resolved at module link time).

7-4-1-2. Overall module structure

The main source file for ChanServ, main.c, follows the same pattern as its NickServ counterpart; see section 7-3-1-2 for a more extensive description.

One of the first things defined in main.c is the chanopts[] table, an array of ChanOpt structures (see above) describing on/off options available on channels from a user's perspective. This table is used primarily for the SET command (thus only flags which have a corresponding SET command are listed), and secondarily for outputting the channel's option set in response to the INFO command. Note that the three HIDE options are not listed here, as they are handled specially for the SET command and not listed in the INFO output.

In addition to the standard command list in the cmds[] array, two separate arrays are defined, for the HALFOP/DEHALFOP and PROTECT/DEPROTECT command pairs. These commands can only be used if the IRC server protocol supports the corresponding feature, so the module initialization routine checks the protocol's feature flags and conditionally registers these two pairs of commands.

ChanServ includes a significant number of callback functions for various IRC events: in addition to watching for newly-created channels and users joining channels, it must also monitor changes of status such as channel modes and channel topics to ensure that they remain consistent with the registered settings and to record changes, as well as take notice of nickname-related events that can affect channels. Of these, the do_nickgroup_delete() callback function, called when a nickname group is deleted, is easily the most complex. Since channel founders must be registered nicknames, the disappearance of a nickname group means that any channels with that group as founder will no longer have a valid founder group ID. If the channel has a successor set, the successor may be able to assume foundership of the channel—but the code must be careful that this does not cause the successor to exceed his registered channel limit, or users could circumvent the limit by registering multiple nicknames, setting one as founder and another as successor, and deliberately dropping the founder nickname. In addition, if the channel was suspended, it is changed to a forbidden channel to prevent users from getting around a suspension by dropping and re-registering their nickname. Because of these various potentialities, the callback function always logs any actions it takes in response to the deletion of a nickname group.

Of the command routines, the basic commands (HELP, REGISTER, IDENTIFY, DROP, DROPCHAN, INFO, LIST) are very similar to their NickServ counterparts, and are not discussed in detail here. The two ChanServ-specific commands whose handlers are fairly complex are AKICK and the OP/VOICE command set.

The AKICK command handler do_akick(), despite its length (including a separate helper routine used with LIST and VIEW), is not dissimilar to the OperServ autokill and S-line commands, or the NickServ ACCESS and AJOIN handlers. The only significant difference is the presence of the ENFORCE subcommand; this is implemented by calling check_kick(), the routine used to check whether newly-joined clients are allowed to join a channel (see section 7-4-1-3) for each client on the channel, causing clients which match an entry on the autokill list to be kickbanned.

The OP and VOICE family of commands all perform a common function—adding or removing a channel user mode—and for this reason, all eight commands are handled by a single routine, do_opvoice(), with handlers for each command that call the common routine with the appropriate command name to indicate the mode of operation. The data used by the common routine is stored in opvoice_data[], an array of structures with the following fields:

When called, do_opvoice() first extracts the data for the command from the opvoice_data[] table, and sets an additional variable, target_nextacc, used for mode-removal commands to set an upper bound on the target client access level check; this is used to, for example, allow DEVOICE on an auto-op client (since the client can just give themselves voice status again if necessary). The routine then loops through all target nicknames given with the command; a do/while loop is used so that if no nicknames are given at all, the code will still be run once (in this case the client that gave the command is used as the target). For each target client, the standard permission and channel status checks are performed, and then the routine determines whether to allow the command:

  1. If the target client is the client giving the command, the command is allowed.
  2. If the command removes a mode (DEOP, DEVOICE, etc.) and the channel does not have the ENFORCE option set, the command is allowed.
  3. If a channel privilege check (target_acc) is not specified for the command, the command is allowed.
  4. If the the target client is not in the privilege class specified for the command's privilege check, the command is allowed.
  5. If an upper limit for the privilege check (target_nextacc) is set for the command and the target client is in that privilege class, the command is allowed.
  6. Otherwise, the command is refused.

Once the command has been allowed, the routine determines which mode flags need to be set or cleared for the target client. (For extensibility, the code allows for more than one flag to be set or cleared for a single command, though the data table only allows one mode character.) If there are no modes to be set or cleared, a notice to that effect is sent to the caller; otherwise, the necessary mode changes are performed, a notice of the mode change is sent to the channel if the OPNOTICE option is enabled, and a success notice is sent to the caller. In addition, if the command was an OP command, the channel's last-used time is updated as for auto-ops.

7-4-1-3. Channel status checking and modification

ChanServ's routines for checking and adjusting channel status are located in the source file check.c. There are six routines exported from this file; two, init_check() and exit_check(), are initialization and cleanup routines called by the module initialization and cleanup code, respectively. The remaining routines are:

void check_modes(Channel *c)
Checks the given channel's modes, making any changes necessary. For registered channels, the "registered" mode (if any) is always added, and other modes are set or cleared according to the mode lock; for unregistered channels, the "registered" mode is always cleared. This routine also checks for the "bouncy modes" phenomenon in tandem with the channel MODE message handle (see section 2-6-3).
void check_chan_user_modes(const char *source, struct c_userlist *u, Channel *c, int32 oldmodes)
Checks the channel user modes of the given client (u) on the given channel, making any changes necessary. The set of "necessary" changes depends not only on the client's current modes, but also on the source of the MODE message that caused the change (passing an empty string for the source parameter will cause such checks to be skipped). oldmodes is the client's previous set of modes, or -1 for a client joining a channel. The sequence of operations is fairly complicated:

The mode changes in these last two steps are performed by a helper routine, local_set_cumodes(), which in turn calls set_cmode() for each mode in the given set.

int check_kick(User *user, const char *chan, int on_join)
Checks whether a client is permitted to be on a channel; if so, returns zero, otherwise kickbans the client and returns nonzero. This routine is normally called when a client joins a channel, before the actual join processing, but setting the on_join parameter to zero allows this routine to be called for clients already in the channel as well, such as for the AKICK ENFORCE command. A client can be denied access to a channel for any number of reasons, checked in the following order:

If the client is to be kickbanned, the routine first checks whether kicking the client would cause the channel to become empty (and thus be deleted, nullifying the effecet of any ban); if so, a JOIN message is sent to the network to cause ChanServ to join the channel, and a timeout for CSInhabit seconds is added to cause ChanServ to leave the channel after that time. Following this, the routine ensures that the ban mask is properly formatted (containing a nickname as well as user and host), then clears any ban exceptions matching the user and adds the ban mask if it is not already present. Once the ban is present, the client is then kicked from the channel, and removed from the internal data structures if necessary.

Implementation note: As mentioned in Appendix D of the user manual, when ChanServ temporarily enters a channel for a kickban, it is not added to the internal channel data; as a result, a subsequent UNBAN or INVITE on the channel will return a "channel does not exist" error. It would probably be better to add ChanServ to the channel's client list like any ordinary client.

int check_topiclock(Channel *c, time_t topic_time)
Called on a channel topic change (topic_time is the timestamp associated with the topic change) to restore the topic to its previous value if the channel's TOPICLOCK option is set. Returns nonzero if the topic is changed by the routine, otherwise zero.
7-4-1-4. The SET and UNSET commands

The handlers for the SET and UNSET commands are located in the set.c source file; as for NickServ, they function like miniature versions of the main chanserv() routine. One noteworthy difference is that the on/off options (other than the three HIDE options) are all handled by a single routine, do_set_boolean(); the main SET handler, do_set(), looks up the option name in the chanopts[] table defined in main.c (checking privileges for the NOEXPIRE option), then calls do_set_boolean(), which uses the data from the ChanOpt structure to set channel flags and send responses to the calling client.

Other noteworthy option handlers are:

do_set_founder()
do_set_successor()
Both SET FOUNDER and SET SUCCESSOR follow the same general pattern: the given nickname is looked up to retrieve its nickname group ID, the ID is checked to ensure that both founder and successor are not set to the same nickname group, and an informational message is logged to record the change. The major difference is that SET FOUNDER checks to ensure that the new founder has not reached the channel registration limit, while SET SUCCESSOR makes no such check. (Even if it did, the check would only make sense at the time the SET SUCCESSOR command was given, and would not reflect any future channel registrations or drops by the successor. An alternative possibility would be to have successor channels count against the channel limit as well, as mentioned in section 11-1.)
do_set_mlock()
The SET MLOCK handler is fairly complex, as it must parse the mode string and parameters to ensure that users cannot inadvertently (or maliciously) cause an invalid MODE message to be sent to the IRC network. The routine parses the mode string character by character; if a mode is found that conflicts with an earlier setting in the string, the later occurrence takes precedence. Additionally, in order to simplify cleanup in case a problem is found, the new mode lock is accumulated in a temporary ModeLock structure, which is copied into the channel's data when the routine successfully completes.

In order to support additional modes provided by particular IRC protocols, do_set_mlock() defines a callback, "SET MLOCK", which is called once for every mode in the mode string; it is also called once after all modes have been processed, to allow for a final validity check (for example, to check for modes that require other modes to be set, as the Unreal protocol module does).

7-4-1-5. ChanServ utility routines

ChanServ's utility functions are defined in util.c. As with NickServ, the preprocessor symbol STANDALONE_CHANSERV can be defined before including util.c in another file; this causes the routines new_channelinfo(), free_channelinfo(), and reset_levels to be defined as static, and eliminates all other code. new_channelinfo() and free_channelinfo() are used to allocate and free resources for a ChannelInfo structure, like their nickname counterparts; reset_levels() resets the privilege levels for a channel (the levels[] array) to default values.

Aside from these three functions (all exported from ChanServ), util.c defines the following routines, along with the initialization and cleanup routines init_util() and exit_util():

int check_channel_limit(const NickGroupInfo *ngi, int *max_ret)
Compares the given nickname group's registered channel count with the limit applied to that nickname, returning -1 if the limit has yet to be reached, 0 if the limit has been reached, and 1 if the limit has been exceeded (much like string comparison functions). Also stores the registered channel limit in the variable pointed to by max_ret if max_ret is not NULL. This routine is exported.
ChannelInfo *makechan(const char *chan)
Creates a new ChannelInfo structure for the given channel name, adds it to the database, and returns it.
int delchan(ChannelInfo *ci)
Removes the given channel from the database, returning nonzero on success, zero on failure.
void count_chan(ChannelInfo *ci)
Updates the NickGroupInfo record for the given channel's founder to indicate that that nickname group owns the channel, incrementing the owned-channel count.
void uncount_chan(ChannelInfo *ci)
Removes the given channel from its founder's owned-channel list and decrements the owned-channel count.
int is_founder(const User *user, const ChannelInfo *ci)
Returns whether the given user has founder access to the given channel, whether due to being the actual channel founder or to identifying for the channel with its founder password.
int is_identified(const User *user, const ChannelInfo *ci)
Returns whether the given user has identified for the channel with its founder password. A subset of is_founder().
void restore_topic(Channel *c)
Restores the saved topic on a newly-created channel if the channel is registered and its KEEPTOPIC option is set.
void record_topic(ChannelInfo *ci, const char *topic, const char *setter, time_t topic_time)
Records the given topic in the given channel's data structure.
void suspend_channel(ChannelInfo *ci, const char *reason, const char *who, const time_t expires)
Suspends the given channel, copying the parameters reason, who, and expires into the suspension data fields. (If expires is zero, then the suspension will not expire.)
void unsuspend_channel(ChannelInfo *ci, int set_time)
Cancels the suspension on the given channel. If set_time is nonzero, the last-used time of the channel will be updated according to CSSuspendGrace to prevent the channel from expiring for that length of time (if CSSuspendGrace or CSExpire are not set, or if the channel already has enough time before expiration, the last-used time will not be changed).
void chan_bad_password(User *u, ChannelInfo *ci)
Records a bad password attempt for the given channel, sending out a WALLOPS if the number of consecutive bad password attempts for the channel reaches the limit specified by the BadPassWarning configuration option (if set).
ChanOpt *chanopt_from_name(const char *optname)
Returns the ChanOpt corresponding to the given (case-insensitive) option name, or NULL if no matching option is found.
char *chanopts_to_string(const ChannelInfo *ci, const NickGroupInfo *ngi)
Returns a string describing the set of options active on the given channel in human-readable form. ngi indicates the client to which the string will be sent and is used in getstring() calls. The returned string is stored in a static buffer, which will be overwritten by the next call to this routine.

7-4-2. Channel access list handling

As discussed in the user's manual, user privileges on channels are maintained via channel access lists. Channel access list handling in ChanServ is split into three files. One, access.c, contains common routines and privilege level definitions, and is included in the chanserv/main module; the other two, access-levels.c and access-xop.c, are independent modules which provide two different ways of manipulating access lists. A separate header file, access.h, contains definitions related to channel access lists.

7-4-2-1. Access list basics

The access.c source file, included as part of the main ChanServ module, serves two main purposes; to define the set of privileges associated with channels, and to provide utility routines for performing common operations related to channel access lists. It also makes use of the access.h header file for certain structure and constant definitions.

The list of channel privileges is defined in the levelinfo[] array at the top of the file; each element in the array is a LevelInfo structure that describes one privilege. (The word "level" in the identifiers comes from the command, LEVELS, used to modify the settings on a per-channel basis; that command name was originally used for the meaning of "setting the levels at which privileges are given".)

The word "privilege" itself is something of a misnomer, as it includes two "negative privileges", CA_AUTODEOP and CA_NOJOIN. While these can be used in the same manner as ordinary privileges, their primary function is in conjunction with the SECUREOPS and RESTRICTED channel options; these cause the respective privilege levels to be treated as zero, preventing users not on the channel access list to be auto-deopped or blocked from entering the channel. However, these two privileges are not visible to users, so "privilege" is considered clear enough to use in the documentation

The LevelInfo structure, defined in access.h, contains the following fields:

int what
The CA_* constant used for this privilege.
int defval
The default channel access level corresponding to this privilege.
const char *name
The user-visible name for this privilege, used in the LEVELS command. An empty string makes the privilege invisible to users.
int desc
The message string index giving the privilege's description.
int action
The "meaning" of the privilege: the action to be performed for clients with the appropriate access level. One of the following flags, any of which may be combined (OR'd) with CL_LESSEQUAL to make the privilege's associated level a maximum rather than a minimum:
union {...} target
Data used in implementing the privilege. The union has two members:

In addition to the levelinfo[] table itself, exported for use by other modules (particularly the http/dbaccess module, described in section 8-2-7), the file's initialization routine init_access() copies relevant parts of the table into two local arrays indexed by privilege (CA_* value): def_levels[], containing each entry's default access level (the defval field), and lev_is_max[], containing a boolean indication of whether the privilege's access level is a maximum (whether the action field has CL_LESSEQUAL set).

The main portion of access.c defines the following utility functions for use by the main ChanServ module and the two access list manipulation modules. Several of the routines are exported for use by external modules as well.

int get_ci_level(const ChannelInfo *ci, int what)
Exported routine. Returns the given channel's level for the given privilege, translating ACCLEV_DEFAULT in the channel's levels[] into the appropriate default value. Returns ACCLEV_INVALID (and logs an error message) on invalid parameters.
int check_access(const User *user, const ChannelInfo *ci, int what)
Exported routine. Returns whether the given client has the given privilege (what) on the given channel. Due to an unfortunate coincidence of terms, this routine can seem confusing to call for the CA_NOJOIN "privilege": the return value will be 1 if the client does not "have access to" the channel, i.e. matches the CA_NOJOIN privilege.
int check_access_if_idented(const User *user, const ChannelInfo *ci, int what)
Exported routine. Like check_access(), but returns what the result would be if the client was identified for its nickname. (If the nickname is not registered, the return value will be the same as for check_access().
int check_access_cmd(const User *user, const ChannelInfo *ci, const char *command, const char *subcommand)
Exported routine. Returns whether the client is allowed to use the given command on the given channel. If the check is being made for a specific subcommand, that subcommand is specified in subcommand; otherwise, subcommand is NULL.
int get_access(const User *user, const ChannelInfo *ci)
Returns the given client's access level on the given channel, taking into account whether the client has identified for its nickname with respect to the nickname and channel SECURE options.
static int get_access_if_idented(const User *user, const ChannelInfo *ci)
Like get_access(), but returns the access level associated with the client's nickname regardless of whether the client has identified or not.
int check_access_cumode(const User *user, const ChannelInfo *ci, int32 newmodes, int32 changemask)
Checks the channel user modes of a client on a channel, returning a bitmask of modes that are to be changed (in other words, the bitwise exclusive-or of the current and resulting mode sets). newmodes is the client's current set of modes on the channel; changemask indicates which modes changed due to the action that caused this function to be called.
int access_add(ChannelInfo *ci, const char *nick, int level, int uacc)
int access_del(ChannelInfo *ci, const char *nick, int uacc)
Adds (access_add()) or deletes (access_del()) an entry to or from the given channel's access list for the nickname group to which nick belongs. level is the access level for the new entry, and uacc is the access level of the client making the change. Both routines return one of the following result codes, defined in cs-local.h: Note that successful return codes are positive, while failure return codes are negative (zero is not used as a return code).

access.c also includes initialization code in the init_access() routine, called from the chanserv/main module's init_module(). This routine initializes the def_levels[] and lev_is_max[] arrays, as well as the flags field of the target.cumode structure for privileges that set or clear modes; it also disables (by removing from the table) any privileges for features not supported by the IRC protocol in use. Implementation note: This relies on the current design of Services, in which the protocol module will never be changed while the program is running. If this design is changed, the code will need to be updated to leave the appropriate entries in the table, perhaps by adding a "disabled" flag or field to each entry.

There is also a corresponding exit_access() routine; it does nothing, but is included for completeness.

7-4-2-2. Manipulation via ACCESS and LEVELS

The chanserv/access-levels module is one of two modules for manipulating channel access lists. It provides direct access to both the channel access list itself, via the ACCESS command, and channels' privilege level settings, via the LEVELS command.

Since the functions available in both commands change depending on runtime parameters, their help messages are handled by a "HELP" callback function, do_help(). In addition to description of the two commands, a third help option, LEVELS DESC provides descriptions of the available channel privileges; unlike most other help messages, the text is generated on the fly from the levelinfo[] array and corresponding description strings.

The ACCESS command handler, do_access(), calls one of several subroutines to perform each of the available actions, depending on the subcommand given. Two of these, do_access_add() and do_access_del(), simply call the access_add() and access_del() routines mentioned in section 7-4-2-1, sending appropriate result messages to the calling client depending on the return codes from those functions. do_access_list() and do_access_listlevel() select access entries for listing based on the given parameters, then call access_list(), defined below the ACCESS subcommand handlers, to actually send the list text to the calling client. (The sent_header parameter to access_list() is used to to record whether a list header message has been sent.) The final subcommand handler, do_access_count(), simply counts up the number of (active) access entries in the list and sends that in a response message to the calling client.

The LEVELS command also has several subcommands, though these are all handled within the command handler do_levels(). The command implementation itself is fairly straightforward, with SET and DISABLE searching the levelinfo[] array for the named privilege, and LIST iterating through that array to display the current level for each privilege.

7-4-2-3. Manipulation via XOP

The chanserv/access-xop module, defined in access-xop.c, provides the XOP command set for managing channel access lists. While these commands present the access list as several distinct lists (SOP, AOP, and so on), they are in fact only different views of the same channel access list, each containing nicknames with a particular predefined access level. This has the following side effects:

The commands themselves are handled by a single central routine, handle_xop(), which is called from each command's particular handler with the appropriate access level. handle_xop() works much like do_access() from access-levels.c (in fact, it is a modified copy of do_access(), and its subroutines are likewise derived from the handlers for the ACCESS subcommands). Response messages include the list name (SOP, AOP, etc.) as a parameter in the message, and this name is determined based on the command's access level using the XOP_LISTNAME macro at the top of the file.


7-5. MemoServ

The MemoServ pseudoclient serves as an adjunct to NickServ, allowing short messages (memos) to be sent between users and storing those messages for the recipient. Like other pseudoclients, the majority of MemoServ's functionality is implemented in the memoserv/main module, described below in section 7-5-1; additional modules allow users to maintain "ignore" lists (memoserv/ignore, section 7-5-2) and have memos forwarded via E-mail (memoserv/forward, section 7-5-3).

7-5-1. MemoServ core functionality

Unlike OperServ, NickServ, and ChanServ, MemoServ only provides additional functionality to users (nicknames and, to an extent, channels) already registered with Services. For this reason, the main MemoServ code is comparatively simple, and the memoserv/main module is implemented by a single source file, main.c. There is also a header file, memoserv.h, containing structures and definitions used by MemoServ.

7-5-1-1. Memo data structures

The structure used to store memos are defined in the header file memoserv.h. Each memo is stored in a structure called (appropriately enough) Memo, and the set of memos belonging to a particular nickname group is stored in a MemoInfo structure. The Memo structure contains the following fields:

uint32 number
The index number associated with this memo, for use with MemoServ commands such as READ and DEL. Note that while the array of memos in a MemoInfo structure is kept free of holes caused by memo deletions, memo index numbers do not change except as a result of the RENUMBER command, so there is not a one-to-one mapping between the two.
int16 flags
Flags associated with the memo. Zero or more of the following constants, OR'd together:
time_t time
The timestamp when the memo was sent.
time_t firstread
The time at which the memo was first read by its recipient. This is stored to ensure that an unread memo does not expire immediately after it is first read (the MSExpireDelay configuration option controls how long MemoServ will wait after this timestamp before expiring the memo).
char sender[NICKMAX]
The nickname of the client that sent the memo.
char *channel
For memos sent to channels, the name of the channel to which the memo was sent. NULL for other memos.
char *text
The text of the memo.

The MemoInfo structure contains:

Memo *memos
int16 memos_count
A variable-length array containing the memos associated with this structure.
int16 memomax
The maximum number of memos this nickname group is allowed to have stored. Either a nonnegative literal value (zero is allowed, and means that the nickname group cannot receive any memos at all), or one of the following constants: The constant MEMOMAX_MAX is also provided to give the largest possible maximum value (a side-effect of storing the value in 16 bits).

Each MemoInfo structure is stored as part of the corresponding nickname group's NickGroupInfo structure (see section 7-3-1-1). In previous versions, channels had associated memo lists as well, but this feature was removed for version 5.1 in favor of the current system of distributing channel memos to privileged users.

There are two more constants in memoserv.h: MS_RECEIVE_PRI_CHECK and MS_RECEIVE_PRI_DELIVER. These are callback priorities that can be used with add_callback_pri() to allow a "receive memo" callback function to ensure it is called before functions which try to deliver the memo. Both the memoserv/ignore and memoserv/forward modules take advantage of these constants.

7-5-1-2. The memoserv/main module

The memoserv/main module is defined in the file main.c, and aside from the lack of any auxiliary source files, its structure is more or less the same as that of the other pseudoclients' core modules.

Since, as mentioned above, the MemoInfo structures containing memo data are stored as part of the corresponding NickGroupInfo structures, special handling is required when moving the data to or from persistent storage. MemoServ uses iterator functions for the MemoInfo table that in turn iterate through NickGroupInfo structures, and sets up a dummy record containing each record's associated nickname group ID, much like the nickserv/access module (see section 7-3-2). For individial Memo records, an additional iterator is used to loop through each memo in every MemoInfo structure.

Most MemoServ actions are implemented by separate routines in the "MemoServ private routines" section of the file, rather than directly in the command handlers. These functions are:

Other utility routines include:

void check_memos(User *u)
Checks whether the given client's nickname group has any unread memos, sending an appropriate message to the client if so (and if the NF_MEMO_SIGNON flag is set for the nickname group). Called by the "user create" callback function do_user_create() if the client is recognized.
void expire_memos(MemoInfo *mi)
Deletes all memos in the given MemoInfo structure that are eligible for expiration.
MemoInfo *get_memoinfo(const char *name, NickGroupInfo **owner_ret, int *error_ret
Returns the MemoInfo structure corresponding to the given nickname, or NULL on error. On success, *owner_ret is set to point to the NickGroupInfo structure for the corresponding nickname group; on error, *error_ret is set to one of the following error codes: Implementation note: This function's primary purpose of handling both nicknames and channel names transparently was obsoleted with the redesign of channel memo handling; however, the function still serves the purpose of checking for unregistered, forbidden, or suspended nicknames, eliminating the necessity to include such checks directly in every command handler.

The command handlers are for the most part simple, only needing to parse parameters and call the appropriate utility routine. The two commands which are implemented directly, SET and INFO, are mostly branching trees for the various nickname options and statuses.

7-5-2. Memo ignore lists

The memoserv/ignore module was added to allow users a way to block memos from unwanted senders such as spammers. The module is implemented by the source file ignore.c.

For the most part, ignore.c works in the same way as the nickserv/access and nickserv/autojoin modules. The one routine unique to memoserv/ignore is the check_if_ignored() routine, added as a callback function to the core MemoServ module's "receive memo" callback at priority MS_RECEIVE_PRI_CHECK. The routine runs through the nickname group's ignore list twoce: the first time, it treats each entry as a Nick!user@host mask which is compared against that of the memo sender, while the second time, it treats each entry as a nickname, and the memo is blocked if that nickname's nickname group ID matches that of the sender (thus proventing malicious clients from evading the block by simply changing to another linked nick). If thd memo is blocked, the routine returns the language string index MEMO_C_GETS_NO_MEMOS (the same as is used if the target has a memo limit of zero), to prevent leaking information about the ignore list contents.

7-5-3. Memo forwarding

The memoserv/forward module, defined in the source file forward.c, provides an interface between memos in Services and an external mail system. As such, one of its prerequisite modules (other than memoserv/main, of course) is nickserv/mail-auth, to reduce the possibility of Services being abused to send mail to arbitrary addresses.

The module includes two methods of forwarding memos: manual and automatic. The former, manual forwarding, is done through the FORWARD command, implemented by the do_forward() routine and its subroutines fwd_memo() and fwd_memo_callback(). As with other commands that allow users to send mail via Services, the command handler includes a check that the command is not used more frequently than the configuration option MSForwardDelay specifies. If this and the other checks pass, the command handler takes one of two actions, depending on its parameter. If "ALL" was given, then fwd_memo() is called for each memo in the calling client's MemoInfo structure; otherwise, the parameter is passed to process_numlist() to call the fwd_memo_callback() callback function (which in turn calls fwd_memo()) for each memo specified in the index list.

fwd_memo() itself does not perform the actual sending of the mail message; rather, it accumulates message body text in its parameters char **p_body and int *p_bodylen (both of which are modified as a result of the routine). This allows the caller to combine multiple memos into a single message without knowing ahead of time how many or which memos are to be sent.

The second method of forwarding, automatic forwarding on receipt, is implemented by the do_receive_memo() function, attached to the MemoServ "receive memo" callback, along with the do_set_forward() option handler (attached to the "SET" callback) to set forwarding options for a nickname group. Of the forwarding options, ON (flag NF_MEMO_FWD) and OFF (no flags set) are fairly obvious. For COPY (flags NF_MEMO_FWD and NF_MEMO_FWDCOPY), however, it is worth noting that (as also mentioned in the user's manual and help messages) memos will be rejected when the recipient has a full list of memos. This is partly a result of the callback implementation, in the sense that the check for a full memo list is made before the callback is ever called; but it also ensures that every memo received by the user is either processed completely (both forwarded and saved) or not at all. This can be important if, for example, a user uses SET FORWARD COPY to save copies of memos to an archival E-mail address, but only uses Services to actually read the memos. If a memo that would overflow the list was nonetheless forwarded and treated as "sent", the sender would be left wondering why the recipient was not responding to the purportedly delivered memo.


7-6. StatServ

The StatServ pseudoclient is intended to record and provide network statistics; however, mostly due to lack of developer interest, it has floundered. The module is defined in the modules/statserv directory by a single source file, main.c, and an accompanying header file, statserv.h.

7-6-1. StatServ data structures

The current implementation of StatServ only records data for servers. This data is stored in a ServerStats structure, defined in statserv.h and containing the following fields:

ServerStats *next, *prev
Used to maintain the linked list of ServerStats structures.
char *name
The name of the server to which this structure applies.
time_t t_join
The timestamp at which the server most recently joined the network. Zero if the server is not currently connected.
time_t t_quit
The timestamp at which the server last disconnected. Zero if Services has never seen the server disconnect.
char *quit_message
The quit message used the last time the server disconnected. NULL if Services has never seen the server disconnect.
int usercnt, opercnt
The current number of clients and IRC operators on the server.

As the data has no direct references to other data stored persistently, saving the ServerStats records is straightforward.

There are also commented-out definitions for MinMax and MinMaxHistory structures, and MinMaxHistory fields in ServerStats; these were originally intended for keeping a history of client and operator counts on each server, but this functionality was never implemented.

7-6-2. The StatServ module

The module itself, defined in main.c, is likewise quite simple. It follows the general layout of other pseudoclient core modules, though it only has two commands (other than HELP) and a few callback routines.

The two commands, SERVERS and USERS, are handled by do_servers() and do_users() respectively. The latter needs no additional explanation, as it only sends the calling client the user and operator counts it maintains internally (via the client-related callbacks mentioned below). do_servers() is only slightly more complex, handling four distinct subcommands. Three of these (STATS, LIST, and VIEW) extract information from the ServerStats structures and send them to the calling client; the last, DELETE, allows a structure to be deleted, and is protected by an is_services_admin() check in the if chain.

In order to keep track of statistics, StatServ naturally relies on callbacks. To watch for connecting and disconnecting servers, StatServ adds callback functions to the core's "server create" and "server delete" callbacks; these functions update the relevant ServerStats structures as servers join and leave the network, with stats_do_server() (the "server create" handler) creating a new ServerStats structure if it sees a server connect that is not recorded in the ServerStats structure table.

StatServ also uses the "user create", "user delete", and "user MODE" callbacks to keep track of the number of clients and IRC operators on the network, stored in the file-scope variables usercnt and opcnt respectively. Note that this overlaps slightly with the maximum client count maintained by OperServ; a good argument could be made for moving this functionality to StatServ as well, but it has been left in OperServ for historical reasons (the maximum client count monitoring functionality has been part of OperServ since the earliest versions of Services, long before StatServ existed).


7-7. Miscellaneous pseudoclients

Aside from the standard pseudoclients listed above, there are two additional pseudoclients, HelpServ and DevNull, provided in case they can be of use. These pseudoclients are disabled in the default Services configuration. These modules are so simple as to not warrant their own subdirectories, and are stored in the modules/misc directory.

7-7-1. HelpServ

The HelpServ pseudoclient, defined in modules/misc/helpserv.c, uses the parameters given in each PRIVMSG it receives to form a pathname for a text file, which (if it exists) is then sent to the calling client as NOTICEs. The pathname is formed by concatenating the path given by the HelpDir module configuration setting (the example configuration file uses helpfiles, relative to the Services data directory) with each of its space-separated parameters, inserting a path separator ("/") between each element. To allow the parameter to be case-insensitive even when using case-sensitive filesystems, all uppercase characters in the parameters are lowercased.

Since this involves access to a file specified by an arbitrary user-specified string, the routine must take care not to allow access to inappropriate files. This is done by replacing all slashes and periods in the string with underscores, to prevent a malicious user from specifying a parameter like "../../../../../etc/passwd". (If there are explicit symbolic links to parent directories, of course, HelpServ will happily follow them.) Implementation note: As a general rule, when sanitizing user input like this it is better to make a set of explicitly allowed characters and delete or convert anything outside that set. However, pathnames on Unix systems are simple and well defined: disregarding the effects of symbolic links, only a slash can allow access to file entries outside of the current directory, and "." and ".." are the only entries that are automatically created within each directory, so the set of allowed characters is essentially "everything except / and .".

In the earliest versions of Services, HelpServ was a standard pseudoclient, and other pseudoclients made use of its functionality to display their own help messages, which were stored as text files under the data/helpfiles directory. However, the help messages were later moved to string data stored directly in the executable file, and then to the current model of language files, diluting the usefulness of HelpServ itself. The module has nonetheless been left in Services in case it is useful for things such as providing network information.

7-7-2. DevNull

The DevNull pseudoclient, defined in modules/misc/devnull.c, is an extremely simple pseudoclient which, like its Unix namesake /dev/null, simply discards any PRIVMSGs sent to it. It is not particularly useful in the ordinary course of events, but the author has made use of it as a default /query target to prevent messages from going to unintended users.