/* PTlink protocol module for IRC Services.
*
* IRC Services is copyright (c) 1996-2007 Andrew Church.
* E-mail: <achurch@achurch.org>
* Parts written by Andrew Kempe and others.
* This program is free but copyrighted software; see the file COPYING for
* details.
*/
#include "services.h"
#include "modules.h"
#include "conffile.h"
#include "language.h"
#include "messages.h"
#include "version.h"
#include "modules/operserv/operserv.h"
#include "modules/nickserv/nickserv.h"
#include "modules/chanserv/chanserv.h"
#include "banexcept.h"
#include "sjoin.h"
#include "svsnick.h"
/*************************************************************************/
/* "Nick" to use to indicate Services-set GLINEs */
#define GLINE_WHO "<ircservices>"
static Module *module;
static Module *module_operserv;
static char *NetworkDomain = NULL;
static int32 usermode_admin = 0; /* +aANT */
static int32 usermode_hiding = 0; /* +S */
static int32 chanmode_admins_only = 0; /* +A */
/*************************************************************************/
/***************** Local interface to external routines ******************/
/*************************************************************************/
static typeof(is_services_admin) *p_is_services_admin = NULL;
static int local_is_services_admin(User *u)
{
return p_is_services_admin && (*p_is_services_admin)(u);
}
#define is_services_admin local_is_services_admin
/*************************************************************************/
/************************** User/channel modes ***************************/
/*************************************************************************/
struct modedata_init {
uint8 mode;
ModeData data;
};
#define MI_ADMIN 0x01000000 /* Usermode given to admins */
#define MI_HIDING 0x02000000 /* Usermode for hiding users */
#define MI_ADMINS_ONLY 0x01000000 /* Chanmode for admin-only channels */
static const struct modedata_init new_usermodes[] = {
{'g', {0x00000008}}, /* Receive globops */
{'h', {0x00000010}}, /* Helpop */
{'r', {0x00000020,0,0,0,MI_REGISTERED}},
/* Registered nick */
{'a', {0x00000040}}, /* Services admin */
{'A', {0x00000080}}, /* Server admin */
{'N', {0x00000100,0,0,0,MI_ADMIN}},
/* Network admin */
{'T', {0x00000200,0,0,0,MI_ADMIN}},
/* Technical admin */
/* Flags in this range are used by Unreal */
{'S', {0x00002000,0,0,0,MI_HIDING}},
/* Stealth mode (hides joins etc) */
{'B', {0x00004000}}, /* Is a bot ("deaf" in Unreal) */
{'R', {0x00008000}}, /* Allow PRIVMSGs from +r clients only */
{'p', {0x00010000}}, /* Private (don't show channels in /whois) */
{'v', {0x00020000}}, /* Don't allow DCCs */
};
static const struct modedata_init new_chanmodes[] = {
{'R', {0x00000100,0,0,0,MI_REGNICKS_ONLY}},
/* Only identified users can join */
{'r', {0x00000200,0,0,0,MI_REGISTERED}},
/* Set for all registered channels */
{'c', {0x00000400,0,0}}, /* No ANSI colors in channel */
{'O', {0x00000800,0,0,0,MI_OPERS_ONLY}},
/* Only opers can join channel */
{'A', {0x00001000,0,0,0,MI_OPERS_ONLY|MI_ADMINS_ONLY}},
/* Only admins can join channel */
/* Flags in this range are used by Unreal */
{'c', {0x00100000,0,0}}, /* Strip colors */
/* Flags in this range are used by Unreal */
{'d', {0x00800000,0,0}}, /* No flooding */
/* Flags in this range are used by Unreal and Bahamut */
{'S', {0x04000000,0,0}}, /* No spam */
{'q', {0x08000000,0,0}}, /* No quits */
{'K', {0x10000000,0,0}}, /* Send knock message on failed join */
{'e', {0x80000000,1,1,0,MI_MULTIPLE}},
/* Ban exceptions */
};
static const struct modedata_init new_chanusermodes[] = {
{'a', {0x00000010,1,1,'.',MI_CHANOWNER}},
/* Channel owner */
};
static void init_modes(void)
{
int i;
for (i = 0; i < lenof(new_usermodes); i++) {
usermodes[new_usermodes[i].mode] = new_usermodes[i].data;
if (new_usermodes[i].data.info & MI_ADMIN)
usermode_admin |= new_usermodes[i].data.flag;
if (new_usermodes[i].data.info & MI_HIDING)
usermode_hiding |= new_usermodes[i].data.flag;
}
for (i = 0; i < lenof(new_chanmodes); i++) {
chanmodes[new_chanmodes[i].mode] = new_chanmodes[i].data;
if (new_chanmodes[i].data.info & MI_ADMINS_ONLY)
chanmode_admins_only |= new_chanmodes[i].data.flag;
}
for (i = 0; i < lenof(new_chanusermodes); i++)
chanusermodes[new_chanusermodes[i].mode] = new_chanusermodes[i].data;
mode_setup();
};
/*************************************************************************/
/************************* IRC message receiving *************************/
/*************************************************************************/
static void m_nick(char *source, int ac, char **av)
{
char *newav[10];
if (*source) {
/* Old user changing nicks. */
if (ac != 2) {
if (debug)
module_log("debug: NICK message: wrong number of parameters"
" (%d) for source `%s'", ac, source);
} else {
do_nick(source, ac, av);
}
return;
}
/* New user. */
if (ac != 9) {
if (debug)
module_log("debug: NICK message: wrong number of parameters (%d)"
" for new user", ac);
return;
}
/* Set up parameters for do_nick() */
newav[0] = av[0]; /* Nick */
newav[1] = av[1]; /* Hop count */
newav[2] = av[2]; /* Timestamp */
newav[3] = av[4]; /* Username */
newav[4] = av[5]; /* Hostname */
newav[5] = av[7]; /* Server */
newav[6] = av[8]; /* Real name */
newav[7] = NULL; /* Services stamp */
newav[8] = NULL; /* IP address */
newav[9] = av[6]; /* User area (fake hostname) */
if (do_nick(source, 10, newav)) {
/* We succeeded, so set modes */
newav[1] = av[3];
do_umode(av[0], 2, newav);
}
}
/*************************************************************************/
static void m_capab(char *source, int ac, char **av)
{
char *s;
int got_PTS4 = 0;
int got_QS = 0;
int got_EX = 0;
if (ac < 1) {
module_log("received CAPAB with no parameters--broken ircd?");
} else {
for (s = strtok(av[0]," "); s; s = strtok(NULL," ")) {
if (stricmp(s, "PTS4") == 0)
got_PTS4 = 1;
else if (stricmp(s, "QS") == 0)
got_QS = 1;
else if (stricmp(s, "EX") == 0)
got_EX = 1;
}
}
if (!got_PTS4 || !got_QS || !got_EX) {
module_log("CAPAB: capabilities missing:%s%s%s",
got_PTS4 ? "" : " PTS4",
got_QS ? "" : " QS",
got_EX ? "" : " EX");
send_error("Need PTS4/QS/EX capabilities");
strscpy(quitmsg, "Remote server doesn't support all of PTS4/QS/EX",
sizeof(quitmsg));
quitting = 1;
}
}
/*************************************************************************/
static void m_svinfo(char *source, int ac, char **av)
{
if (ac < 2) {
module_log("received SVINFO with <2 parameters--broken ircd?");
send_error("Invalid SVINFO received (at least 2 parameters needed)");
quitting = 1;
} else {
if (atoi(av[1]) > 6 || atoi(av[0]) < 6) {
send_error("Need protocol version 6 support");
strscpy(quitmsg,
"Remote server doesn't support protocol version 6",
sizeof(quitmsg));
quitting = 1;
}
}
}
/*************************************************************************/
static void m_sjoin(char *source, int ac, char **av)
{
if (ac < 4) {
module_log("SJOIN: expected >=4 params, got %d (broken ircd?)", ac);
return;
}
do_sjoin(source, ac, av);
}
/*************************************************************************/
static void m_newmask(char *source, int ac, char **av)
{
char *newuser, *newhost;
User *u;
if (ac < 1) {
module_log("NEWUSER: parameters missing--broken ircd?");
return;
}
if (!(u = get_user(source))) {
module_log("got NEWUSER from nonexistent user %s", source);
return;
}
newuser = av[0];
newhost = strchr(newuser, '@');
if (newhost)
*newhost++ = 0;
else
newhost = "";
free(u->username);
u->username = sstrdup(newuser);
free(u->host);
u->host = sstrdup(newhost);
}
/*************************************************************************/
/* GLINE/SGLINE/SQLINE handling: cancel any Services-set lines from remote
* servers (see comments in unreal.c/m_tkl() for details).
*/
static void m_gline(char *source, int ac, char **av)
{
if (ac < 3 || strcmp(av[2], GLINE_WHO) != 0)
return;
send_cmd(ServerName, "UNGLINE :%s", av[0]);
}
static void m_sgline(char *source, int ac, char **av)
{
int masklen;
if (ac < 3)
return;
masklen = atoi(av[1]);
if (masklen < strlen(av[2]))
av[2][masklen] = 0;
send_cmd(ServerName, "UNSGLINE :%s", av[2]);
}
static void m_sqline(char *source, int ac, char **av)
{
if (ac < 1)
return;
send_cmd(ServerName, "UNSQLINE :%s", av[0]);
}
/*************************************************************************/
static Message ptlink_messages[] = {
{ "CAPAB", m_capab },
{ "GLINE", m_gline },
{ "GLOBOPS", NULL },
{ "GNOTICE", NULL },
{ "GOPER", NULL },
{ "NEWMASK", m_newmask },
{ "NICK", m_nick },
{ "SILENCE", NULL },
{ "SJOIN", m_sjoin },
{ "SGLINE", m_sgline },
{ "SQLINE", m_sqline },
{ "SVINFO", m_svinfo },
{ "SVLINE", NULL },
{ "UNGLINE", NULL },
{ "UNSGLINE", NULL },
{ "UNSQLINE", NULL },
{ "UNSVLINE", NULL },
{ "UNZOMBIE", NULL },
{ "ZOMBIE", NULL },
{ NULL }
};
/*************************************************************************/
/************************** IRC message sending **************************/
/*************************************************************************/
/* Send a NICK command for a new user. */
static void do_send_nick(const char *nick, const char *user, const char *host,
const char *server, const char *name,
const char *modes)
{
/* NICK <nick> <hops> <TS> <umode> <user> <host> <fakehost> <server>
* :<ircname> */
send_cmd(NULL, "NICK %s 1 %ld +%s %s %s %s %s 0 :%s", nick,
(long)time(NULL), modes, user, host, host, server, name);
}
/*************************************************************************/
/* Send a NICK command to change an existing user's nick. */
static void do_send_nickchange(const char *nick, const char *newnick)
{
send_cmd(nick, "NICK %s %ld", newnick, (long)time(NULL));
}
/*************************************************************************/
/* Send a command to change a user's "real name". */
static void do_send_namechange(const char *nick, const char *newname)
{
/* Not supported by this protocol. */
}
/*************************************************************************/
/* Send a SERVER command, and anything else needed at the beginning of the
* connection.
*/
static void do_send_server(void)
{
send_cmd(NULL, "PASS %s :TS", RemotePassword);
send_cmd(NULL, "CAPAB :PTS4 QS EX");
send_cmd(NULL, "SERVER %s 1 ircservices-%s :%s", ServerName,
version_number, ServerDesc);
send_cmd(NULL, "SVINFO 6 6");
send_cmd(NULL, "SVSINFO %ld %d", (long)start_time, maxusercnt);
}
/*************************************************************************/
/* Send a SERVER command for a remote (juped) server. */
static void do_send_server_remote(const char *server, const char *reason)
{
send_cmd(NULL, "SERVER %s 2 :%s", server, reason);
}
/*************************************************************************/
/* Send a WALLOPS (really a GLOBOPS). */
static void do_wallops(const char *source, const char *fmt, ...)
{
va_list args;
char buf[BUFSIZE];
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
send_cmd(source ? source : ServerName, "GLOBOPS :%s", buf);
}
/*************************************************************************/
/* Send a NOTICE to all users on the network. */
static void do_notice_all(const char *source, const char *fmt, ...)
{
va_list args;
char msgbuf[BUFSIZE];
va_start(args, fmt);
vsnprintf(msgbuf, sizeof(msgbuf), fmt, args);
va_end(args);
if (NetworkDomain) {
send_cmd(source, "NOTICE $*.%s :%s", NetworkDomain, msgbuf);
} else {
/* Go through all common top-level domains. If you have others,
* add them here. */
send_cmd(source, "NOTICE $*.com :%s", msgbuf);
send_cmd(source, "NOTICE $*.net :%s", msgbuf);
send_cmd(source, "NOTICE $*.org :%s", msgbuf);
send_cmd(source, "NOTICE $*.edu :%s", msgbuf);
}
}
/*************************************************************************/
/* Send a command which modifies channel status. */
static void do_send_channel_cmd(const char *source, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
vsend_cmd(ServerName, fmt, args);
va_end(args);
}
/*************************************************************************/
/******************************* Callbacks *******************************/
/*************************************************************************/
static int do_user_create(User *user, int ac, char **av)
{
user->fakehost = sstrdup(av[9]);
return 0;
}
/*************************************************************************/
static int do_user_mode(User *user, int modechar, int add, char **av)
{
switch (modechar) {
case 'o':
if (add) {
user->mode |= UMODE_o;
if (add && user_identified(user) && is_services_admin(user))
send_cmd(ServerName, "SVSMODE %s +a", user->nick);
user->mode &= ~UMODE_o;
}
return 0;
case 'r':
if (user_identified(user)) {
if (!add)
send_cmd(ServerName, "SVSMODE %s +r", user->nick);
} else {
if (add)
send_cmd(ServerName, "SVSMODE %s -r", user->nick);
}
return 1;
case 'a':
if (is_oper(user)) {
if (is_services_admin(user)) {
if (!add)
send_cmd(ServerName, "SVSMODE %s +a", user->nick);
} else {
if (add)
send_cmd(ServerName, "SVSMODE %s -a", user->nick);
}
return 1;
}
}
return 0;
}
/*************************************************************************/
static int do_nick_identified(User *u, int old_status)
{
if (is_oper(u) && is_services_admin(u))
send_cmd(ServerName, "SVSMODE %s +a", u->nick);
return 0;
}
/*************************************************************************/
static int do_set_topic(const char *source, Channel *c, const char *topic,
const char *setter, time_t t)
{
if (setter)
return 0;
c->topic_time = t;
send_cmd(source, "TOPIC %s %s %ld :%s", c->name, c->topic_setter,
(long)c->topic_time, c->topic ? c->topic : "");
return 1;
}
/*************************************************************************/
static int do_check_chan_user_modes(const char *source, User *user,
Channel *c, int32 modes)
{
/* Don't do anything to hiding users */
return ((user->mode & usermode_hiding) ? 1 : 0);
}
/*************************************************************************/
static int do_check_kick(User *user, const char *chan, ChannelInfo *ci,
char **mask_ret, const char **reason_ret)
{
Channel *c = get_channel(chan);
/* Don't let plain opers into +A (admin only) channels */
if ((((c?c->mode:0) | (ci?ci->mlock_on:0)) & chanmode_admins_only)
&& !(user->mode & usermode_admin)
) {
*mask_ret = create_mask(user, 1);
*reason_ret = getstring(user->ngi, CHAN_NOT_ALLOWED_TO_JOIN);
return 1;
}
return 0;
}
/*************************************************************************/
static int do_send_akill(const char *username, const char *host,
time_t expires, const char *who, const char *reason)
{
time_t now = time(NULL);
send_cmd(ServerName, "GLINE %s@%s %ld %s :%s", username, host,
(long)((expires && expires > now) ? expires - now : 0),
GLINE_WHO, reason);
return 1;
}
/*************************************************************************/
static int do_cancel_akill(const char *username, const char *host)
{
send_cmd(ServerName, "UNGLINE %s@%s", username, host);
return 1;
}
/*************************************************************************/
static int do_send_sgline(const char *mask, time_t expires, const char *who,
const char *reason)
{
send_cmd(ServerName, "SGLINE %d :%s:%s", (int)strlen(mask), mask, reason);
return 1;
}
static int do_send_sqline(const char *mask, time_t expires, const char *who,
const char *reason)
{
send_cmd(ServerName, "SQLINE %s :%s", mask, reason);
return 1;
}
/*************************************************************************/
static int do_cancel_sgline(const char *mask)
{
send_cmd(ServerName, "UNSGLINE :%s", mask);
return 1;
}
static int do_cancel_sqline(const char *mask)
{
send_cmd(ServerName, "UNSQLINE %s", mask);
return 1;
}
/*************************************************************************/
/***************************** Module stuff ******************************/
/*************************************************************************/
const int32 module_version = MODULE_VERSION_CODE;
ConfigDirective module_config[] = {
{ "NetworkDomain", { { CD_STRING, 0, &NetworkDomain } } },
SJOIN_CONFIG,
{ NULL }
};
/*************************************************************************/
static int do_load_module(Module *mod, const char *modname)
{
if (strcmp(modname, "operserv/main") == 0) {
module_operserv = mod;
p_is_services_admin = get_module_symbol(mod, "is_services_admin");
if (!p_is_services_admin) {
module_log("warning: unable to look up symbol `is_services_admin'"
" in module `operserv/main'");
}
} else if (strcmp(modname, "operserv/akill") == 0) {
if (!add_callback(mod, "send_akill", do_send_akill))
module_log("Unable to add send_akill callback");
if (!add_callback(mod, "cancel_akill", do_cancel_akill))
module_log("Unable to add cancel_akill callback");
} else if (strcmp(modname, "operserv/sline") == 0) {
if (!add_callback(mod, "send_sgline", do_send_sgline))
module_log("Unable to add send_sgline callback");
if (!add_callback(mod, "send_sqline", do_send_sqline))
module_log("Unable to add send_sqline callback");
if (!add_callback(mod, "cancel_sgline", do_cancel_sgline))
module_log("Unable to add cancel_sgline callback");
if (!add_callback(mod, "cancel_sqline", do_cancel_sqline))
module_log("Unable to add cancel_sqline callback");
} else if (strcmp(modname, "nickserv/main") == 0) {
if (!add_callback(mod, "identified", do_nick_identified))
module_log("Unable to add NickServ identified callback");
} else if (strcmp(modname, "chanserv/main") == 0) {
if (!add_callback(mod, "check_chan_user_modes",
do_check_chan_user_modes))
module_log("Unable to add ChanServ check_chan_user_modes"
" callback");
if (!add_callback(mod, "check_kick", do_check_kick))
module_log("Unable to add ChanServ check_kick callback");
}
return 0;
}
/*************************************************************************/
static int do_unload_module(Module *mod)
{
if (mod == module_operserv) {
module_operserv = NULL;
p_is_services_admin = NULL;
}
return 0;
}
/*************************************************************************/
int init_module(Module *module_)
{
unsigned char c;
module = module_;
protocol_name = "PTlink";
protocol_version = "6.x";
protocol_features = PF_BANEXCEPT | PF_NOQUIT;
protocol_nickmax = 20;
if (!register_messages(ptlink_messages)) {
module_log("Unable to register messages");
exit_module(1);
return 0;
}
if (!add_callback(NULL, "load module", do_load_module)
|| !add_callback(NULL, "unload module", do_unload_module)
|| !add_callback(NULL, "user create", do_user_create)
|| !add_callback(NULL, "user MODE", do_user_mode)
|| !add_callback(NULL, "set topic", do_set_topic)
) {
module_log("Unable to add callbacks");
exit_module(1);
return 0;
}
if (!init_banexcept(module) || !init_sjoin(module)
|| !init_svsnick(module)
) {
exit_module(1);
return 0;
}
init_modes();
irc_lowertable['['] = '[';
irc_lowertable['\\'] = '\\';
irc_lowertable[']'] = ']';
for (c = 'A'; c <= '}'; c++)
valid_nick_table[c] = 3;
for (c = 0; c < 32; c++)
valid_chan_table[c] = 0;
valid_chan_table[160] = 0;
send_nick = do_send_nick;
send_nickchange = do_send_nickchange;
send_namechange = do_send_namechange;
send_server = do_send_server;
send_server_remote = do_send_server_remote;
wallops = do_wallops;
notice_all = do_notice_all;
send_channel_cmd = do_send_channel_cmd;
pseudoclient_modes = "";
enforcer_modes = "";
setstring(OPER_BOUNCY_MODES, OPER_BOUNCY_MODES_U_LINE);
return 1;
}
/*************************************************************************/
int exit_module(int shutdown)
{
if (!shutdown) {
/* Do not allow removal */
return 0;
}
exit_svsnick();
exit_sjoin();
exit_banexcept();
remove_callback(NULL, "set topic", do_set_topic);
remove_callback(NULL, "user MODE", do_user_mode);
remove_callback(NULL, "user create", do_user_create);
remove_callback(NULL, "unload module", do_unload_module);
remove_callback(NULL, "load module", do_load_module);
unregister_messages(ptlink_messages);
return 1;
}
/*************************************************************************/
syntax highlighted by Code2HTML, v. 0.9.1