1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-07-04 13:13:13 +02:00
Files
unrealircd/src/modules/nick.c
T
Bram Matthys d79161019a Clear client->local->proto for users.
This is not an issue now in all code paths, but if someone accidentally uses
SupportXYZ() without checking IsServer() then it would be an issue.

In the past we used client->local->proto for client flags as well, but this
has been split off to client->local->caps a while ago.

I guess we should rename client->local->proto to something more server-ish
in a later major release to indicate this as well.
2026-02-22 10:37:01 +01:00

1446 lines
44 KiB
C

/*
* IRC - Internet Relay Chat, src/modules/nick.c
* (C) 1999-2005 The UnrealIRCd Team
*
* See file AUTHORS in IRC package for additional names of
* the programmers.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 1, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "unrealircd.h"
ModuleHeader MOD_HEADER
= {
"nick",
"5.0",
"command /nick",
"UnrealIRCd Team",
"unrealircd-6",
};
/* Defines */
#define NICKCOL_EQUAL 0
#define NICKCOL_NEW_WON 1
#define NICKCOL_EXISTING_WON 2
/* Assume that on collision a NICK is in flight and the other server will take
* the exact same decision we would do, and thus we don't send a KILL to cptr?
* This works great with this code, seems to kill the correct person and not
* cause desyncs even without UID/SID. HOWEVER.. who knows what code the other servers run?
* Should use UID/SID anyway, then this whole problem doesn't exist.
*/
#define ASSUME_NICK_IN_FLIGHT
static char spamfilter_user[NICKLEN + USERLEN + HOSTLEN + REALLEN + 64];
/* Forward declarations */
CMD_FUNC(cmd_nick);
CMD_FUNC(cmd_nick_local);
CMD_FUNC(cmd_nick_remote);
CMD_FUNC(cmd_uid);
int _register_user(Client *client);
void nick_collision(Client *cptr, const char *newnick, const char *newid, Client *new, Client *existing, int type);
int AllowClient(Client *client);
char *_unreal_expand_string(const char *str, char *buf, size_t buflen, NameValuePrioList *nvp, int buildvarstring_options, Client *client);
MOD_TEST()
{
MARK_AS_OFFICIAL_MODULE(modinfo);
EfunctionAdd(modinfo->handle, EFUNC_REGISTER_USER, _register_user);
EfunctionAddString(modinfo->handle, EFUNC_UNREAL_EXPAND_STRING, _unreal_expand_string);
return MOD_SUCCESS;
}
MOD_INIT()
{
MARK_AS_OFFICIAL_MODULE(modinfo);
CommandAdd(modinfo->handle, "NICK", cmd_nick, MAXPARA, CMD_USER|CMD_SERVER|CMD_UNREGISTERED);
CommandAdd(modinfo->handle, "UID", cmd_uid, MAXPARA, CMD_SERVER);
return MOD_SUCCESS;
}
MOD_LOAD()
{
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
return MOD_SUCCESS;
}
/** Hmm.. don't we already have such a function? */
void set_user_modes_dont_spread(Client *client, const char *umode)
{
const char *args[4];
args[0] = NULL;
args[1] = client->id;
args[2] = umode;
args[3] = NULL;
dontspread = 1;
cmd_umode(NULL, client, NULL, 3, args);
dontspread = 0;
}
/** Can user use this nick?
* This is only 1 part of many checks you would need to do.
* You should also run the nick through do_nick_name before this,
* and after this also check with find_qline() and find_client().
*/
int can_use_nick(Client *client, const char *nick)
{
Hook *h;
for (h = Hooks[HOOKTYPE_CAN_USE_NICK]; h; h = h->next)
{
char *change_nick_error_from_hook = NULL;
int ret = (*(h->func.intfunc))(client, nick, &change_nick_error_from_hook);
if (ret == HOOK_DENY)
{
if (change_nick_error_from_hook)
sendnumeric(client, ERR_ERRONEUSNICKNAME, nick, change_nick_error_from_hook);
else
sendnumeric(client, ERR_ERRONEUSNICKNAME, nick, "Denied by hook");
return 0;
}
}
return 1;
}
/** Remote client (already fully registered) changing their nick */
CMD_FUNC(cmd_nick_remote)
{
TKL *tklban;
int ishold;
Client *acptr;
char nick[NICKLEN + 2];
char oldnick[NICKLEN + 1];
time_t lastnick = 0;
int differ = 1;
unsigned char removemoder = (client->umodes & UMODE_REGNICK) ? 1 : 0;
MessageTag *mtags = NULL;
/* 'client' is always the fully registered user doing the nick change */
strlcpy(nick, parv[1], NICKLEN + 1);
strlcpy(oldnick, client->name, sizeof(oldnick));
if (parc > 2)
lastnick = atol(parv[2]);
if (!do_remote_nick_name(nick) || !strcasecmp("ircd", nick) || !strcasecmp("irc", nick))
{
ircstats.is_kill++;
unreal_log(ULOG_ERROR, "nick", "BAD_NICK_REMOTE", client,
"Server link $server tried to change '$client' to bad nick '$nick' -- rejected.",
log_data_string("nick", parv[1]),
log_data_client("server", client->uplink));
mtags = NULL;
new_message(client, NULL, &mtags);
sendto_one(client, mtags, ":%s KILL %s :Illegal nick name", me.id, client->id);
SetKilled(client);
exit_client(client, mtags, "Illegal nick name");
free_message_tags(mtags);
mtags = NULL;
return;
}
/* Check Q-lines / ban nick */
if (!IsULine(client) && (tklban = find_qline(client, nick, &ishold)) && !ishold)
{
unreal_log(ULOG_INFO, "nick", "QLINE_NICK_REMOTE", client,
"Banned nick $nick used by $client.details, allowed through by server $server ($reason)",
log_data_string("nick", parv[1]),
log_data_string("ip", GetIP(client)),
log_data_client("server", client->uplink),
log_data_string("reason", tklban->ptr.nameban->reason));
/* Let it through */
}
if ((acptr = find_client(nick, NULL)))
{
/* If existing nick is still in handshake, kill it */
if (IsUnknown(acptr) && MyConnect(acptr))
{
SetKilled(acptr);
exit_client(acptr, NULL, "Overridden");
} else
if (acptr == client)
{
/* 100% identical? Must be a bug, but ok */
if (!strcmp(acptr->name, nick))
return;
/* Allows change of case in their nick */
removemoder = 0; /* don't set the user -r */
} else
{
/*
** A NICK change has collided (e.g. message type ":old NICK new").
*/
differ = (mycmp(acptr->user->username, client->user->username) ||
mycmp(acptr->user->realhost, client->user->realhost));
if (!(parc > 2) || lastnick == acptr->lastnick)
{
nick_collision(client, parv[1], client->id, client, acptr, NICKCOL_EQUAL);
return; /* Now that I killed them both, ignore the NICK */
} else
if ((differ && (acptr->lastnick > lastnick)) ||
(!differ && (acptr->lastnick < lastnick)))
{
nick_collision(client, parv[1], client->id, client, acptr, NICKCOL_NEW_WON);
/* fallthrough: their user won, continue and proceed with the nick change */
} else
if ((differ && (acptr->lastnick < lastnick)) ||
(!differ && (acptr->lastnick > lastnick)))
{
nick_collision(client, parv[1], client->id, client, acptr, NICKCOL_EXISTING_WON);
return; /* their user lost, ignore the NICK */
} else
{
return; /* just in case */
}
}
}
mtags = NULL;
if (!IsULine(client))
{
unreal_log(ULOG_INFO, "nick", "REMOTE_NICK_CHANGE", client,
"Client $client.details has changed their nickname to $new_nick",
log_data_string("new_nick", nick));
}
new_message(client, recv_mtags, &mtags);
RunHook(HOOKTYPE_REMOTE_NICKCHANGE, client, mtags, nick);
client->lastnick = lastnick ? lastnick : TStime();
add_history(client, 1, WHOWAS_EVENT_NICK_CHANGE);
sendto_server(client, 0, 0, mtags, ":%s NICK %s %lld",
client->id, nick, (long long)client->lastnick);
sendto_local_common_channels(client, client, 0, mtags, ":%s NICK :%s", client->name, nick);
if (removemoder)
client->umodes &= ~UMODE_REGNICK;
/* Finally set new nick name. */
del_from_client_hash_table(client->name, client);
strlcpy(client->name, nick, sizeof(client->name));
add_to_client_hash_table(nick, client);
RunHook(HOOKTYPE_POST_REMOTE_NICKCHANGE, client, mtags, oldnick);
free_message_tags(mtags);
}
/* Local user: either setting their nick for the first time (registration)
* or changing their nick (fully registered already, or not)
*/
CMD_FUNC(cmd_nick_local)
{
TKL *tklban;
int ishold;
Client *acptr;
char nick[NICKLEN + 2];
char oldnick[NICKLEN + 1];
char descbuf[BUFSIZE];
Membership *mp;
int newuser = 0;
unsigned char removemoder = (client->umodes & UMODE_REGNICK) ? 1 : 0;
Hook *h;
int ret;
strlcpy(oldnick, client->name, sizeof(oldnick));
/* Enforce minimum nick length */
if (iConf.min_nick_length && !IsOper(client) && !IsULine(client) && strlen(parv[1]) < iConf.min_nick_length)
{
snprintf(descbuf, sizeof descbuf, "A minimum length of %d chars is required", iConf.min_nick_length);
sendnumeric(client, ERR_ERRONEUSNICKNAME, parv[1], descbuf);
return;
}
/* Enforce maximum nick length */
strlcpy(nick, parv[1], iConf.nick_length + 1);
/* Check if this is a valid nick name */
if (!do_nick_name(nick))
{
sendnumeric(client, ERR_ERRONEUSNICKNAME, parv[1], "Illegal characters");
return;
}
/* Check for collisions / in use */
if (!strcasecmp("ircd", nick) || !strcasecmp("irc", nick))
{
sendnumeric(client, ERR_ERRONEUSNICKNAME, nick, "Reserved for internal IRCd purposes");
return;
}
if (MyUser(client))
{
/* Local client changing nick: check spamfilter */
spamfilter_build_user_string(spamfilter_user, nick, client);
if (match_spamfilter(client, spamfilter_user, SPAMF_USER, "NICK", NULL, 0, clictx, NULL))
return;
}
if (!can_use_nick(client, nick))
return;
/* Check Q-lines / ban nick */
if (!IsULine(client) && (tklban = find_qline(client, nick, &ishold)))
{
if (ishold)
{
sendnumeric(client, ERR_ERRONEUSNICKNAME, nick, tklban->ptr.nameban->reason);
return;
}
if (!ValidatePermissionsForPath("immune:server-ban:ban-nick",client,NULL,NULL,nick))
{
add_fake_lag(client, 4000); /* lag them up */
sendnumeric(client, ERR_ERRONEUSNICKNAME, nick, tklban->ptr.nameban->reason);
unreal_log(ULOG_INFO, "nick", "QLINE_NICK_LOCAL_ATTEMPT", client,
"Attempt to use banned nick $nick by $client.details blocked ($reason)",
log_data_string("nick", parv[1]),
log_data_string("ip", GetIP(client)),
log_data_client("server", client->uplink),
log_data_string("reason", tklban->ptr.nameban->reason));
return; /* NICK message ignored */
}
/* fallthrough for ircops that have sufficient privileges */
}
if (!ValidatePermissionsForPath("immune:nick-flood",client,NULL,NULL,NULL))
add_fake_lag(client, 3000);
if ((acptr = find_client(nick, NULL)))
{
/* Shouldn't be possible since dot is disallowed: */
if (IsServer(acptr))
{
sendnumeric(client, ERR_NICKNAMEINUSE, nick);
return;
}
if (acptr == client)
{
/* New nick is exactly the same as the old nick? */
if (!strcmp(acptr->name, nick))
return;
/* Changing cAsE */
removemoder = 0;
} else
/* Collision with a nick of a session that is still in handshake */
if (IsUnknown(acptr) && MyConnect(acptr))
{
/* Kill the other connection that is still in progress */
SetKilled(acptr);
exit_client(acptr, NULL, "Overridden");
} else
{
sendnumeric(client, ERR_NICKNAMEINUSE, nick);
return; /* NICK message ignored */
}
}
/* set::anti-flood::nick-flood */
if (client->user &&
!ValidatePermissionsForPath("immune:nick-flood",client,NULL,NULL,NULL) &&
flood_limit_exceeded(client, FLD_NICK))
{
/* Throttle... */
sendnumeric(client, ERR_NCHANGETOOFAST, nick);
return;
}
/* New local client? */
if (!client->name[0])
{
newuser = 1;
if (iConf.ping_cookie)
{
/*
* Client setting NICK the first time.
*
* Generate a random string for them to pong with.
*/
client->local->nospoof = getrandom32();
sendto_one(client, NULL, "PING :%X", client->local->nospoof);
}
/* Copy password to the passwd field if it's given after NICK */
if ((parc > 2) && (strlen(parv[2]) <= PASSWDLEN))
safe_strdup(client->local->passwd, parv[2]);
/* This had to be copied here to avoid problems.. */
strlcpy(client->name, nick, sizeof(client->name));
/* Let's see if we can get online now... */
if (is_handshake_finished(client))
{
/* Send a CTCP VERSION */
if (!iConf.ping_cookie && USE_BAN_VERSION && MyConnect(client))
sendto_one(client, NULL, ":IRC!IRC@%s PRIVMSG %s :\1VERSION\1", me.name, nick);
client->lastnick = TStime();
if (!register_user(client))
{
if (IsDead(client))
return;
/* ..otherwise.. fallthrough so we run the same code
* as in case of !is_handshake_finished()
*/
} else {
/* New user! */
strlcpy(nick, client->name, sizeof(nick)); /* don't ask, but I need this. do not remove! -- Syzop */
}
}
} else
if (MyUser(client))
{
MessageTag *mtags = NULL;
int ret;
/* Existing client nick-changing */
/*
** If the client belongs to me, then check to see
** if client is currently on any channels where it
** is currently banned. If so, do not allow the nick
** change to occur.
** Also set 'lastnick' to current time, if changed.
*/
for (mp = client->user->channel; mp; mp = mp->next)
{
int ret = HOOK_CONTINUE;
Hook *h;
if (!check_channel_access(client, mp->channel, "hoaq") && is_banned(client, mp->channel, BANCHK_NICK, NULL, NULL))
{
sendnumeric(client, ERR_BANNICKCHANGE,
mp->channel->name);
return;
}
if (CHECK_TARGET_NICK_BANS && !check_channel_access(client, mp->channel, "hoaq") && is_banned_with_nick(client, mp->channel, BANCHK_NICK, nick, NULL, NULL))
{
sendnumeric(client, ERR_BANNICKCHANGE, mp->channel->name);
return;
}
for (h = Hooks[HOOKTYPE_CHAN_PERMIT_NICK_CHANGE]; h; h = h->next)
{
ret = (*(h->func.intfunc))(client,mp->channel);
if (ret != HOOK_CONTINUE)
break;
}
if (ret == HOOK_DENY)
{
sendnumeric(client, ERR_NONICKCHANGE, mp->channel->name);
return;
}
}
unreal_log(ULOG_INFO, "nick", "LOCAL_NICK_CHANGE", client,
"Client $client.details has changed their nickname to $new_nick",
log_data_string("new_nick", nick));
new_message(client, recv_mtags, &mtags);
RunHook(HOOKTYPE_LOCAL_NICKCHANGE, client, mtags, nick);
add_history(client, 1, WHOWAS_EVENT_NICK_CHANGE);
client->lastnick = TStime(); /* needs to be done AFTER add_history() */
sendto_server(client, 0, 0, mtags, ":%s NICK %s %lld",
client->id, nick, (long long)client->lastnick);
sendto_local_common_channels(client, client, 0, mtags, ":%s NICK :%s", client->name, nick);
sendto_one(client, mtags, ":%s NICK :%s", client->name, nick);
free_message_tags(mtags);
if (removemoder)
client->umodes &= ~UMODE_REGNICK;
} else
{
/* Someone changing nicks in the pre-registered phase */
}
del_from_client_hash_table(client->name, client);
strlcpy(client->name, nick, sizeof(client->name));
add_to_client_hash_table(nick, client);
/* update fdlist --nenolod */
snprintf(descbuf, sizeof(descbuf), "Client: %s", nick);
fd_desc(client->local->fd, descbuf);
if (removemoder && MyUser(client))
sendto_one(client, NULL, ":%s MODE %s :-r", me.name, client->name);
if (MyUser(client) && !newuser)
RunHook(HOOKTYPE_POST_LOCAL_NICKCHANGE, client, recv_mtags, oldnick);
}
/*
** cmd_uid
** parv[1] = nickname
** parv[2] = hopcount
** parv[3] = timestamp
** parv[4] = username
** parv[5] = hostname
** parv[6] = UID
** parv[7] = account name (SVID)
** parv[8] = umodes
** parv[9] = virthost, * if none
** parv[10] = cloaked host, * if none
** parv[11] = ip
** parv[12] = info
**
** Technical documentation is available at:
** https://www.unrealircd.org/docs/Server_protocol:UID_command
*/
CMD_FUNC(cmd_uid)
{
TKL *tklban;
int ishold;
Client *acptr, *serv = NULL;
Client *acptrs;
char nick[NICKLEN + 1];
char buf[BUFSIZE];
long lastnick = 0;
int differ = 1;
const char *hostname, *username, *sstamp, *umodes, *virthost, *ip_raw, *realname;
const char *ip = NULL;
if (parc < 13)
{
sendnumeric(client, ERR_NEEDMOREPARAMS, "UID");
return;
}
/* It's not just the directly attached client which must be a
* server. The source itself needs to be a server.
*/
if (!IsServer(client))
{
sendnumeric(client, ERR_NOTFORUSERS, "UID");
return;
}
strlcpy(nick, parv[1], sizeof(nick));
hostname = parv[5];
sstamp = parv[7];
username = parv[4];
umodes = parv[8];
virthost = parv[9];
ip_raw = parv[11];
realname = parv[12];
/* Do some *MINIMAL* nick name checking for remote nicknames.
* This will only catch things that severely break things. -- Syzop
*/
if (!do_remote_nick_name(nick) || !strcasecmp("ircd", nick) || !strcasecmp("irc", nick))
{
unreal_log(ULOG_ERROR, "nick", "BAD_NICK_REMOTE", client->uplink,
"Server link $client tried to introduce bad nick '$nick' -- rejected.",
log_data_string("nick", parv[1]));
sendnumeric(client, ERR_ERRONEUSNICKNAME, parv[1], "Illegal nick name");
ircstats.is_kill++;
/* Send kill to uplink only, hasn't been broadcasted to the rest, anyway */
sendto_one(client, NULL, ":%s KILL %s :Bad nick", me.id, parv[1]);
return;
}
if (!valid_uid(parv[6]) || strncmp(parv[6], client->id, 3))
{
ircstats.is_kill++;
unreal_log(ULOG_ERROR, "link", "BAD_UID", client,
"Server link $client ($sid) used bad UID $uid in UID command.",
log_data_string("sid", client->id),
log_data_string("uid", parv[6]));
/* Send kill to uplink only, hasn't been broadcasted to the rest, anyway */
sendto_one(client, NULL, ":%s KILL %s :Bad UID", me.id, parv[6]);
return;
}
if (!valid_host(hostname, 0))
{
ircstats.is_kill++;
unreal_log(ULOG_ERROR, "link", "BAD_HOSTNAME", client,
"Server link $client ($client.id) introduced user $nick with bad host name: $bad_hostname.",
log_data_string("nick", nick),
log_data_string("bad_hostname", hostname));
/* Send kill to uplink only, hasn't been broadcasted to the rest, anyway */
sendto_one(client, NULL, ":%s KILL %s :Bad hostname", me.id, parv[6]);
return;
}
if (strcmp(virthost, "*") && !valid_host(virthost, 0))
{
ircstats.is_kill++;
unreal_log(ULOG_ERROR, "link", "BAD_HOSTNAME", client,
"Server link $client ($client.id) introduced user $nick with bad virtual hostname: $bad_hostname.",
log_data_string("nick", nick),
log_data_string("bad_hostname", virthost));
/* Send kill to uplink only, hasn't been broadcasted to the rest, anyway */
sendto_one(client, NULL, ":%s KILL %s :Bad virtual host", me.id, parv[6]);
return;
}
if (strcmp(ip_raw, "*"))
{
if (!(ip = decode_ip(ip_raw)))
{
ircstats.is_kill++;
unreal_log(ULOG_ERROR, "link", "BAD_IP", client,
"Server link $client ($client.id) introduced user $nick with bad IP: $bad_ip.",
log_data_string("nick", nick),
log_data_string("bad_ip", ip_raw));
/* Send kill to uplink only, hasn't been broadcasted to the rest, anyway */
sendto_one(client, NULL, ":%s KILL %s :Bad IP in UID command", me.id, parv[6]);
return;
}
}
/* Kill quarantined opers early... */
if (IsQuarantined(client->direction) && strchr(parv[8], 'o'))
{
ircstats.is_kill++;
/* Send kill to uplink only, hasn't been broadcasted to the rest, anyway */
unreal_log(ULOG_INFO, "link", "OPER_KILLED_QUARANTINE", NULL,
"QUARANTINE: Oper $nick on server $server killed, due to quarantine",
log_data_string("nick", parv[1]),
log_data_client("server", client));
sendto_one(client, NULL, ":%s KILL %s :Quarantined: no oper privileges allowed", me.id, parv[6]);
return;
}
if (!IsULine(client) && (tklban = find_qline(client, nick, &ishold)))
{
unreal_log(ULOG_INFO, "nick", "QLINE_NICK_REMOTE", client,
"Banned nick $nick [$nick.ip] from server $server ($reason)",
log_data_string("nick", parv[1]),
log_data_string("ip", ip),
log_data_client("server", client->uplink),
log_data_string("reason", tklban->ptr.nameban->reason));
/* Let it through */
}
/* Now check if 'nick' already exists - collision with a user (or still in handshake, unknown) */
if ((acptr = find_client(nick, NULL)) != NULL)
{
/* If there's a collision with a user that is still in handshake, on our side,
* then we can just kill our client and continue.
*/
if (MyConnect(acptr) && IsUnknown(acptr))
{
SetKilled(acptr);
exit_client(acptr, NULL, "Overridden");
goto nickkill2done;
}
lastnick = atol(parv[3]);
differ = (mycmp(acptr->user->username, parv[4]) || mycmp(acptr->user->realhost, parv[5]));
if (acptr->lastnick == lastnick)
{
nick_collision(client, parv[1], parv[6], NULL, acptr, NICKCOL_EQUAL);
return; /* We killed both users, now stop the process. */
}
if ((differ && (acptr->lastnick > lastnick)) ||
(!differ && (acptr->lastnick < lastnick)) || acptr->direction == client->direction) /* we missed a QUIT somewhere ? */
{
nick_collision(client, parv[1], parv[6], NULL, acptr, NICKCOL_NEW_WON);
/* We got rid of the "wrong" user. Introduce the correct one. */
/* ^^ hmm.. why was this absent in nenolod's code, resulting in a 'return 0'? seems wrong. */
goto nickkill2done;
}
if ((differ && (acptr->lastnick < lastnick)) || (!differ && (acptr->lastnick > lastnick)))
{
nick_collision(client, parv[1], parv[6], NULL, acptr, NICKCOL_EXISTING_WON);
return; /* Ignore the NICK */
}
return; /* just in case */
}
nickkill2done:
/* Proceed with introducing the new client, change source (replace client) */
serv = client;
client = make_client(serv->direction, serv);
strlcpy(client->id, parv[6], sizeof(client->id));
add_client_to_list(client);
add_to_id_hash_table(client->id, client);
client->lastnick = atol(parv[3]);
strlcpy(client->name, nick, NICKLEN+1);
add_to_client_hash_table(nick, client);
make_user(client);
/* Note that cloaked host aka parv[10] is unused */
client->user->server = find_or_add(client->uplink->name);
strlcpy(client->user->realhost, hostname, sizeof(client->user->realhost));
if (ip)
{
if (!set_client_ip(client, ip))
{
/* This should not be possible as we validate the 'ip' with
* the call to decode_ip() about 100 lines up.
*/
unreal_log(ULOG_ERROR, "nick", "REMOTE_CLIENT_IP_BUG", client,
"[BUG] client $client has invalid ip $ip -- rejected",
log_data_string("ip", ip));
#ifdef DEBUGMODE
abort();
#endif
/* This could leave a ghost / unsynched, but it should never happen... right? */
return;
}
}
if (*sstamp != '*')
strlcpy(client->user->account, sstamp, sizeof(client->user->account));
strlcpy(client->info, realname, sizeof(client->info));
strlcpy(client->user->username, username, USERLEN + 1);
SetUser(client);
make_cloakedhost(client, client->user->realhost, client->user->cloakedhost, sizeof(client->user->cloakedhost));
safe_strdup(client->user->virthost, client->user->cloakedhost);
/* Inherit flags from server, makes it easy in the send routines
* and this also makes clients inherit ulines.
*/
client->flags |= client->uplink->flags;
/* Update counts */
irccounts.clients++;
if (client->uplink->server)
client->uplink->server->users++;
if (client->umodes & UMODE_INVISIBLE)
irccounts.invisible++;
/* Set user modes */
set_user_modes_dont_spread(client, umodes);
/* Set the vhost */
if (*virthost != '*')
safe_strdup(client->user->virthost, virthost);
build_umode_string(client, 0, SEND_UMODES|UMODE_SERVNOTICE, buf);
sendto_serv_butone_nickcmd(client->direction, recv_mtags, client, (*buf == '\0' ? "+" : buf));
moddata_extract_s2s_mtags(client, recv_mtags);
if (IsLoggedIn(client))
{
user_account_login(recv_mtags, client);
/* no need to check for kill upon user_account_login() here
* since that can only happen for local users.
*/
}
update_known_user_cache(client);
RunHook(HOOKTYPE_REMOTE_CONNECT, client);
if (!IsULine(serv) && IsSynched(serv))
{
unreal_log(ULOG_INFO, "connect", "REMOTE_CLIENT_CONNECT", client,
"Client connecting: $client ($client.user.username@$client.hostname) [$client.ip] $extended_client_info",
log_data_string("extended_client_info", get_connect_extinfo(client)),
log_data_string("from_server_name", client->user->server));
}
}
/** The NICK command.
* In UnrealIRCd 4 and later this should only happen for:
* 1) A local user setting or changing the nick name ("NICK xyz")
* -> cmd_nick_local()
* 2) A remote user changing their nick name (":<uid> NICK <newnick>")
* -> cmd_nick_remote()
*/
CMD_FUNC(cmd_nick)
{
if ((parc < 2) || BadPtr(parv[1]))
{
sendnumeric(client, ERR_NONICKNAMEGIVEN);
return;
}
if (MyConnect(client) && !IsServer(client))
{
CALL_CMD_FUNC(cmd_nick_local);
} else
if (!IsUser(client))
{
unreal_log(ULOG_ERROR, "link", "LINK_OLD_PROTOCOL_NICK", client->direction,
"Server link $client tried to introduce $nick using NICK command. "
"Server is using an old and unsupported protocol from UnrealIRCd 3.2.x or earlier, should use the UID command. "
"See https://www.unrealircd.org/docs/FAQ#old-server-protocol",
log_data_string("nick", parv[1]));
/* Split the entire uplink, as it should never have allowed this (and probably they are to blame too) */
exit_client(client->direction, NULL, "Server used NICK command, bad, must use UID!");
return;
} else
{
CALL_CMD_FUNC(cmd_nick_remote);
}
}
/** Welcome the user on IRC.
* Send the RPL_WELCOME, LUSERS, MOTD, auto join channels, everything...
*/
void welcome_user(Client *client, TKL *viruschan_tkl)
{
int i;
ConfigItem_tld *tld;
char *chans;
char buf[BUFSIZE];
/* Make creation time the real 'online since' time, excluding registration time.
* Otherwise things like set::anti-spam-quit-messagetime 10s could mean
* 1 second in practice (#2174).
*/
client->local->creationtime = TStime();
client->local->idle_since = TStime();
RunHook(HOOKTYPE_WELCOME, client, 0);
sendnumeric(client, RPL_WELCOME, NETWORK_NAME, client->name, client->user->username, client->user->realhost);
RunHook(HOOKTYPE_WELCOME, client, 1);
sendnumeric(client, RPL_YOURHOST, me.name, version);
RunHook(HOOKTYPE_WELCOME, client, 2);
sendnumeric(client, RPL_CREATED, creation);
RunHook(HOOKTYPE_WELCOME, client, 3);
sendnumeric(client, RPL_MYINFO, me.name, version, umodestring, cmodestring);
RunHook(HOOKTYPE_WELCOME, client, 4);
send_isupport(client);
RunHook(HOOKTYPE_WELCOME, client, 5);
if (IsHidden(client))
{
sendnumeric(client, RPL_HOSTHIDDEN, client->user->virthost);
RunHook(HOOKTYPE_WELCOME, client, 396);
}
if (IsSecureConnect(client))
{
if (client->local->ssl && !iConf.no_connect_tls_info)
{
sendnotice(client, "*** You are connected to %s with %s",
me.name, tls_get_cipher(client));
}
}
{
const char *parv[2];
parv[0] = NULL;
parv[1] = NULL;
do_cmd(client, NULL, "LUSERS", 1, parv);
if (IsDead(client))
return;
}
RunHook(HOOKTYPE_WELCOME, client, 266);
short_motd(client);
RunHook(HOOKTYPE_WELCOME, client, 376);
#ifdef EXPERIMENTAL
sendnotice(client,
"*** \2NOTE:\2 This server is running experimental IRC server software (UnrealIRCd %s). "
"If you find any bugs or problems, please report them at https://bugs.unrealircd.org/",
VERSIONONLY);
#endif
if (client->umodes & UMODE_INVISIBLE)
irccounts.invisible++;
build_umode_string(client, 0, SEND_UMODES|UMODE_SERVNOTICE, buf);
sendto_serv_butone_nickcmd(client->direction, NULL, client, (*buf == '\0' ? "+" : buf));
broadcast_moddata_client(client);
if (buf[0] != '\0' && buf[1] != '\0')
sendto_one(client, NULL, ":%s MODE %s :%s", client->name,
client->name, buf);
if (client->user->snomask)
sendnumeric(client, RPL_SNOMASK, client->user->snomask);
if (!IsSecure(client) && !IsLocalhost(client) && (iConf.plaintext_policy_user == POLICY_WARN))
sendnotice_multiline(client, iConf.plaintext_policy_user_message);
if (IsSecure(client) && (iConf.outdated_tls_policy_user == POLICY_WARN) && outdated_tls_client(client))
sendnotice(client, "%s", outdated_tls_client_build_string(iConf.outdated_tls_policy_user_message, client));
RunHook(HOOKTYPE_LOCAL_CONNECT, client);
/* Give the user a fresh start as far as fake-lag is concerned.
* Otherwise the user could be lagged up already due to all the CAP stuff.
*/
client->local->fake_lag = TStime();
RunHook(HOOKTYPE_WELCOME, client, 999);
/* NOTE: Code after this 'if (viruschan_tkl)' will not be executed for quarantined-
* virus-users. So be carefull with the order. -- Syzop
*/
// FIXME: verify if this works, trace code path upstream!!!!
if (viruschan_tkl)
{
join_viruschan(client, viruschan_tkl, SPAMF_USER);
return;
}
/* Force the user to join the given chans -- codemastr */
tld = find_tld(client);
if (tld && !BadPtr(tld->channel))
{
char *chans = strdup(tld->channel);
const char *args[3] = {
NULL,
chans,
NULL
};
do_cmd(client, NULL, "JOIN", 3, args);
safe_free(chans);
if (IsDead(client))
return;
}
else if ((chans = (char *)get_setting_for_user_string(client, SET_AUTO_JOIN)) && strcmp(chans, "0"))
{
chans = strdup(chans); // work on a copy (wait, is this still needed? we have a const guarantee now right?)
const char *args[3] = {
NULL,
chans,
NULL
};
do_cmd(client, NULL, "JOIN", 3, args);
safe_free(chans);
if (IsDead(client))
return;
}
}
/** Make a valid client->user->username, or try to anyway.
* @param client The client to check
* @param noident Whether we should ignore the first ~ or not
* @returns 1 if the username is acceptable, 0 if not.
* @note This function will modify client->user->username to make it valid.
* Only if there are zero valid characters it will return 0.
* @note There is also valid_username() in src/misc.c
*/
int make_valid_username(Client *client, int noident)
{
static char stripuser[USERLEN + 1];
char *i;
char *o = stripuser;
char filtered = 0; /* any changes? */
*stripuser = '\0';
for (i = client->user->username + noident; *i; i++)
{
if (isallowed(*i))
*o++ = *i;
else
filtered = 1;
}
*o = '\0';
if (filtered == 0)
return 1; /* No change needed, all good */
if (*stripuser == '\0')
return 0; /* Zero valid characters, reject it */
strlcpy(client->user->username + 1, stripuser, sizeof(client->user->username)-1);
client->user->username[0] = '~';
client->user->username[USERLEN] = '\0';
return 1; /* Filtered, but OK */
}
/** Register the connection as a User - only for local connections!
* This is called after NICK + USER (in no particular order)
* and possibly other protocol messages as well (eg CAP).
* @param client Client to be made a user.
* @returns 1 if successfully registered, 0 if not (client might be killed).
*/
int _register_user(Client *client)
{
ConfigItem_ban *bconf;
char *tmpstr;
char noident = 0;
int i;
Hook *h;
TKL *savetkl = NULL;
char temp[USERLEN + 1];
char descbuf[BUFSIZE];
if (!MyConnect(client))
abort();
if (IsUser(client))
{
/* register_user() was called but we are already
* a registered user. This is not good. If we are
* running in DEBUGMODE we will abort/crash.
* Otherwise, we return 0.
*/
unreal_log(ULOG_ERROR, "user", "BUG_REGISTER_USER_CALLED_TWICE", client,
"[BUG] register_user() was called more than once on user $client.details");
#ifdef DEBUGMODE
abort();
#else
return 0;
#endif
}
/* Don't allow shunned users in */
if (IsShunned(client))
return 0;
/* Set client->local->sockhost:
* First deal with the special 'localhost' case and
* then with generic setting based on DNS.
*/
if (!strcmp(GetIP(client), "127.0.0.1") ||
!strcmp(GetIP(client), "0:0:0:0:0:0:0:1") ||
!strcmp(GetIP(client), "0:0:0:0:0:ffff:127.0.0.1"))
{
set_sockhost(client, "localhost");
if (client->local->hostp)
{
unreal_free_hostent(client->local->hostp);
client->local->hostp = NULL;
}
} else
{
struct hostent *hp = client->local->hostp;
if (hp && hp->h_name)
set_sockhost(client, hp->h_name);
}
/* Set the hostname (client->user->realhost).
* This may later be overwritten by the AllowClient() call to
* revert to the IP again if allow::options::useip is set.
*/
strlcpy(client->user->realhost, client->local->sockhost, sizeof(client->local->sockhost));
/* Now that it is going to be a user, clear any PROTOCTL flags, as those are meant for servers */
client->local->proto = 0;
/* Check allow { } blocks... */
if (!AllowClient(client))
{
ircstats.is_ref++;
/* For safety, we have an extra kill here */
if (!IsDead(client))
exit_client(client, NULL, "Rejected");
return 0;
}
if (IsUseIdent(client))
{
if (IsIdentSuccess(client))
{
/* ident succeeded: overwite client->user->username with the ident reply */
strlcpy(client->user->username, client->ident, sizeof(client->user->username));
} else
if (IDENT_CHECK)
{
/* ident check is enabled and it failed: prefix the username with ~ */
char temp[USERLEN+1];
strlcpy(temp, client->user->username, sizeof(temp));
snprintf(client->user->username, sizeof(client->user->username), "~%s", temp);
noident = 1;
}
}
/* Now validate the username. This may alter client->user->username
* or reject it completely.
*/
if (!make_valid_username(client, noident))
{
exit_client(client, NULL, "Hostile username. Please use only 0-9 a-z A-Z _ - and . in your username.");
return 0;
}
/* Check ban realname { } blocks */
if ((bconf = find_ban(NULL, client->info, CONF_BAN_REALNAME)))
{
ircstats.is_ref++;
banned_client(client, "realname", bconf->reason?bconf->reason:"", 0, 0);
return 0;
}
/* Check G/Z lines before shuns -- kill before quite -- codemastr */
if (find_tkline_match(client, 0))
{
if (!IsDead(client) && client->local->class)
{
/* Fix client count bug, in case that it was a hold such as via authprompt */
client->local->class->clients--;
client->local->class = NULL;
}
ircstats.is_ref++;
return 0;
}
find_shun(client);
spamfilter_build_user_string(spamfilter_user, client->name, client);
if (match_spamfilter(client, spamfilter_user, SPAMF_USER, NULL, NULL, 0, NULL, &savetkl))
{
if (savetkl &&
(has_actions_of_type(savetkl->ptr.spamfilter->action, BAN_ACT_VIRUSCHAN) ||
has_actions_of_type(savetkl->ptr.spamfilter->action, BAN_ACT_SOFT_VIRUSCHAN)))
{
/* 'viruschan' action:
* Continue with registering the client, and at the end
* of this function we will do the actual joining to the
* virus channel.
*/
} else {
/* Client is either dead or blocked (will hang, on purpose, and timeout) */
return 0;
}
}
for (h = Hooks[HOOKTYPE_PRE_LOCAL_CONNECT]; h; h = h->next)
{
int ret = (*(h->func.intfunc))(client);
if (ret == HOOK_DENY)
{
if (!IsDead(client) && client->local->class)
{
/* Fix client count bug, in case that
* the HOOK_DENY was only meant temporarily.
*/
client->local->class->clients--;
client->local->class = NULL;
}
return 0;
}
if (ret == HOOK_ALLOW)
break;
}
/* User is going to be accepted -- don't reject the user anymore under this line! */
SetUser(client);
/* set::modes-on-connect - needs to be here for account-based custom set settings (PR #270) */
client->umodes |= get_setting_for_user_number(client, SET_MODES_ON_CONNECT);
make_cloakedhost(client, client->user->realhost, client->user->cloakedhost, sizeof(client->user->cloakedhost));
/* client->user->virthost should never be empty */
if (!IsSetHost(client) || !client->user->virthost)
safe_strdup(client->user->virthost, client->user->cloakedhost);
snprintf(descbuf, sizeof descbuf, "Client: %s", client->name);
fd_desc(client->local->fd, descbuf);
/* Move user from unknown list to client list */
list_move(&client->lclient_node, &lclient_list);
/* Update counts */
irccounts.unknown--;
irccounts.clients++;
irccounts.me_clients++;
if (client->uplink && client->uplink->server)
client->uplink->server->users++;
if (IsSecure(client))
{
client->umodes |= UMODE_SECURE;
RunHook(HOOKTYPE_SECURE_CONNECT, client);
}
safe_free(client->local->passwd);
unreal_log(ULOG_INFO, "connect", "LOCAL_CLIENT_CONNECT", client,
"Client connecting: $client ($client.user.username@$client.hostname) [$client.ip] $extended_client_info",
log_data_string("extended_client_info", get_connect_extinfo(client)));
/* Send the RPL_WELCOME, LUSERS, MOTD, auto join channels, everything... */
welcome_user(client, savetkl);
return IsDead(client) ? 0 : 1;
}
/** Nick collission detected. A winner has been decided upstream. Deal with killing.
* I moved this all to a single routine here rather than having all code duplicated
* due to SID vs NICK and some code quadruplicated.
*/
void nick_collision(Client *cptr, const char *newnick, const char *newid, Client *new, Client *existing, int type)
{
char comment[512];
const char *new_server, *existing_server;
const char *who_won;
const char *nickcol_reason;
if (type == NICKCOL_NEW_WON)
who_won = "new";
else if (type == NICKCOL_EXISTING_WON)
who_won = "existing";
else
who_won = "none";
nickcol_reason = new ? "nick change" : "new user connecting";
unreal_log(ULOG_ERROR, "nick", "NICK_COLLISION", NULL,
"Nick collision: "
"$new_nick[$new_id]@$uplink (new) vs "
"$existing_client[$existing_client.id]@$existing_client.user.servername (existing). "
"Winner: $nick_collision_winner. "
"Cause: $nick_collision_reason",
log_data_string("new_nick", newnick),
log_data_string("new_id", newid),
log_data_client("uplink", cptr),
log_data_client("existing_client", existing),
log_data_string("nick_collision_winner", who_won),
log_data_string("nick_collision_reason", nickcol_reason));
new_server = cptr->name;
existing_server = (existing == existing->direction) ? me.name : existing->direction->name;
if (type == NICKCOL_EXISTING_WON)
snprintf(comment, sizeof(comment), "Nick collision: %s <- %s", new_server, existing_server);
else if (type == NICKCOL_NEW_WON)
snprintf(comment, sizeof(comment), "Nick collision: %s <- %s", existing_server, new_server);
else
snprintf(comment, sizeof(comment), "Nick collision: %s <-> %s", existing_server, new_server);
/* We only care about the direction from this point, not about the originally sending server */
cptr = cptr->direction;
if ((type == NICKCOL_EQUAL) || (type == NICKCOL_EXISTING_WON))
{
/* Kill 'new':
* - 'new' is known by the cptr-side as 'newnick' already
* - if not nick-changing then the other servers don't know this user
* - if nick-changing, then the the other servers know the user as new->name
*/
/* cptr case first... this side knows the user by newnick/newid */
/* SID server can kill 'new' by ID */
sendto_one(cptr, NULL, ":%s KILL %s :%s", me.id, newid, comment);
/* non-cptr case... only necessary if nick-changing. */
if (new)
{
MessageTag *mtags = NULL;
new_message(new, NULL, &mtags);
/* non-cptr side knows this user by their old nick name */
sendto_server(cptr, 0, 0, mtags, ":%s KILL %s :%s", me.id, new->id, comment);
/* Exit the client */
ircstats.is_kill++;
SetKilled(new);
exit_client(new, mtags, comment);
free_message_tags(mtags);
}
}
if ((type == NICKCOL_EQUAL) || (type == NICKCOL_NEW_WON))
{
MessageTag *mtags = NULL;
new_message(existing, NULL, &mtags);
/* Now let's kill 'existing' */
sendto_server(NULL, 0, 0, mtags, ":%s KILL %s :%s", me.id, existing->id, comment);
/* Exit the client */
ircstats.is_kill++;
SetKilled(existing);
exit_client(existing, mtags, comment);
free_message_tags(mtags);
}
}
/** Allow or reject the client based on allow { } blocks and all other restrictions.
* @param client Client to check (local)
* @param username Username, for some reason...
* @returns 1 if OK, 0 if client is rejected (likely killed too)
*/
int AllowClient(Client *client)
{
static char sockhost[HOSTLEN + 1];
int i;
ConfigItem_allow *aconf;
char *hname;
static char uhost[HOSTLEN + USERLEN + 3];
static char fullname[HOSTLEN + 1];
Hook *h;
if (!IsSecure(client) && !IsLocalhost(client) && (iConf.plaintext_policy_user == POLICY_DENY))
{
exit_client(client, NULL, iConf.plaintext_policy_user_message->line);
return 0;
}
if (IsSecure(client) && (iConf.outdated_tls_policy_user == POLICY_DENY) && outdated_tls_client(client))
{
const char *msg = outdated_tls_client_build_string(iConf.outdated_tls_policy_user_message, client);
exit_client(client, NULL, msg);
return 0;
}
for (aconf = conf_allow; aconf; aconf = aconf->next)
{
if (aconf->flags.tls && !IsSecure(client))
continue;
if (!user_allowed_by_security_group(client, aconf->match))
continue;
/* Check authentication */
if (aconf->auth && !Auth_Check(client, aconf->auth, client->local->passwd))
{
/* Incorrect password/authentication - but was is it required? */
if (aconf->flags.reject_on_auth_failure)
{
exit_client(client, NULL, iConf.reject_message_unauthorized);
return 0;
} else {
continue; /* Continue (this is the default behavior) */
}
}
if (!aconf->flags.noident)
SetUseIdent(client);
if (aconf->flags.useip)
set_sockhost(client, GetIP(client));
update_known_user_cache(client);
for (h = Hooks[HOOKTYPE_ALLOW_CLIENT]; h; h = h->next)
{
const char *reject_reason = (*(h->func.stringfunc))(client, aconf);
if (reject_reason)
{
exit_client(client, NULL, reject_reason);
return 0;
}
}
if (!((aconf->class->clients + 1) > aconf->class->maxclients))
{
client->local->class = aconf->class;
client->local->class->clients++;
}
else
{
/* Class is full */
sendnumeric(client, RPL_REDIR, aconf->server ? aconf->server : DEFAULT_SERVER, aconf->port ? aconf->port : 6667);
exit_client(client, NULL, iConf.reject_message_server_full);
return 0;
}
return 1;
}
/* User did not match any allow { } blocks: */
exit_client(client, NULL, iConf.reject_message_unauthorized);
return 0;
}
/** Expand 'str' which contains '$variables' into 'buf'.
* @param str The string which may contain $variables
* @param buf The buffer to store the result in
* @param buflen The size of 'buf'
* @param nvp A list with custom extra variables, or simply NULL.
* Note that the list is freed at the end, including the custom settings.
* @param buildvarstring_options One or more of BUILDVARSTRING_* or simply 0.
* @param client The client to expand details from
* @returns A pointer to 'buf'
* @notes If you do provide a non-NULL 'nvp' then note that the entire nvp list is FREED before return
* including your custom list!
*/
char *_unreal_expand_string(const char *str, char *buf, size_t buflen, NameValuePrioList *nvp, int buildvarstring_options, Client *client)
{
const char *s;
if (client)
{
add_nvplist(&nvp, 0, "nick", client->name);
add_nvplist(&nvp, 0, "servername", client->uplink->name);
add_nvplist(&nvp, 0, "server", client->uplink->name); /* (old backwards compatible name) */
add_nvplist(&nvp, 0, "ip", GetIP(client));
if (client->user && *client->user->realhost)
add_nvplist(&nvp, 0, "hostname", client->user->realhost);
else if (client->local && *client->local->sockhost)
add_nvplist(&nvp, 0, "hostname", client->local->sockhost);
else
add_nvplist(&nvp, 0, "hostname", GetIP(client));
if (client->user)
{
add_nvplist(&nvp, 0, "username", client->user->username);
add_nvplist(&nvp, 0, "realname", client->info);
add_nvplist(&nvp, 0, "account", client->user->account);
s = get_operlogin(client);
if (s)
add_nvplist(&nvp, 0, "operlogin", s);
s = get_operclass(client);
if (s)
add_nvplist(&nvp, 0, "operclass", s);
}
if (client->ip)
{
GeoIPResult *geo = geoip_client(client);
if (geo)
{
char asn[32];
if (geo->country_code)
add_nvplist(&nvp, 0, "country_code", geo->country_code);
else
add_nvplist(&nvp, 0, "country_code", "XX");
/* Safe to set this unconditionally, will simply be 0
* for things like localhost and such.
*/
snprintf(asn, sizeof(asn), "%d", geo->asn);
add_nvplist(&nvp, 0, "asn", asn);
} else {
add_nvplist(&nvp, 0, "country_code", "XX");
add_nvplist(&nvp, 0, "asn", "0");
}
}
}
buildvarstring_nvp(str, buf, buflen, nvp, buildvarstring_options);
safe_free_nvplist(nvp);
return buf;
}