/*
 *   Unreal Internet Relay Chat Daemon, src/serv.c
 *   Copyright (C) 1990 Jarkko Oikarinen and
 *                      University of Oulu, Computing Center
 *
 *   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.
 */

/** @file
 * @brief Server-related functions
 */

/* s_serv.c 2.55 2/7/94 (C) 1988 University of Oulu, Computing Center and Jarkko Oikarinen */

#include "unrealircd.h"
#include <ares.h>
#ifndef _WIN32
/* for uname(), is POSIX so should be OK... */
#include <sys/utsname.h>
#endif

MODVAR int  max_connection_count = 1, max_client_count = 1;
extern int do_garbage_collect;
/* We need all these for cached MOTDs -- codemastr */
extern char *buildid;
MOTDFile opermotd;
MOTDFile rules;
MOTDFile motd;
MOTDFile svsmotd;
MOTDFile botmotd;
MOTDFile smotd;

/** Hash list of TKL entries */
MODVAR TKL *tklines[TKLISTLEN];
/** 2D hash list of TKL entries + IP address */
MODVAR TKL *tklines_ip_hash[TKLIPHASHLEN1][TKLIPHASHLEN2];
int MODVAR spamf_ugly_vchanoverride = 0;

void read_motd(const char *filename, MOTDFile *motd);
void do_read_motd(const char *filename, MOTDFile *themotd);

extern MOTDLine *find_file(char *, short);

void reread_motdsandrules();

#if defined(__GNUC__)
/* Temporarily ignore for this function. FIXME later!!! */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif

/** Send a message upstream if necessary and check if it's for us.
 * @param client	The sender
 * @param mtags		Message tags associated with this message
 * @param command	The command (eg: "NOTICE")
 * @param server	This indicates parv[server] contains the destination
 * @param parc		Parameter count (MAX 8!!)
 * @param parv		Parameter values (MAX 8!!)
 * @note While sending parv[server] is replaced with the name of the matched client
 *       (virtually, as parv[] is not actually written to)
 */
int hunt_server(Client *client, MessageTag *mtags, const char *command, int server, int parc, const char *parv[])
{
	Client *acptr;
	const char *saved;
	int i;
	char buf[1024];

	if (strchr(command, '%') || strchr(command, ' '))
	{
		unreal_log(ULOG_ERROR, "main", "BUG_HUNT_SERVER", client,
		           "[BUG] hunt_server called with command '$command' but it may not contain "
		           "spaces or percentage signs nowadays, it must be ONLY the command.",
		           log_data_string("command", command));
		abort();
	}

	/* This would be strange and bad. Previous version assumed "it's for me". Hmm.. okay. */
	if (parc <= server || BadPtr(parv[server]))
		return HUNTED_ISME;

	acptr = find_client(parv[server], NULL);

	/* find_client() may find a variety of clients. Only servers/persons please, no 'unknowns'. */
	if (acptr && MyConnect(acptr) && !IsMe(acptr) && !IsUser(acptr) && !IsServer(acptr))
		acptr = NULL;

	if (!acptr)
	{
		sendnumeric(client, ERR_NOSUCHSERVER, parv[server]);
		return HUNTED_NOSUCH;
	}

	if (IsMe(acptr) || MyUser(acptr))
		return HUNTED_ISME;

	/* Never send the message back from where it came from */
	if (acptr->direction == client->direction)
	{
		sendnumeric(client, ERR_NOSUCHSERVER, parv[server]);
		return HUNTED_NOSUCH;
	}

	/* This puts all parv[] arguments in 'buf'
	 * Taken from concat_params() but this one is
	 * with parv[server] magic replacement.
	 */
	*buf = '\0';
	for (i = 1; i < parc; i++)
	{
		const char *param = parv[i];

		if (!param)
			break;

		/* The magic parv[server] replacement:
		 * this replaces eg 'User' with '001' in S2S traffic.
		 */
		if (i == server)
			param = acptr->id;

		if (*buf)
			strlcat(buf, " ", sizeof(buf));

		if (strchr(param, ' ') || (*param == ':'))
		{
			/* Last parameter, with : */
			strlcat(buf, ":", sizeof(buf));
			strlcat(buf, parv[i], sizeof(buf));
			break;
		}
		strlcat(buf, parv[i], sizeof(buf));
	}

	sendto_one(acptr, mtags, ":%s %s %s", client->id, command, buf);

	return HUNTED_PASS;
}

#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif

#ifndef _WIN32
/** Grab operating system name on Windows (outdated) */
char *getosname(void)
{
	static char buf[1024];
	struct utsname osinf;
	char *p;

	memset(&osinf, 0, sizeof(osinf));
	if (uname(&osinf) != 0)
		return "<unknown>";
	snprintf(buf, sizeof(buf), "%s %s %s %s %s",
		osinf.sysname,
		osinf.nodename,
		osinf.release,
		osinf.version,
		osinf.machine);
	/* get rid of cr/lf */
	for (p=buf; *p; p++)
		if ((*p == '\n') || (*p == '\r'))
		{
			*p = '\0';
			break;
		}
	return buf;
}
#endif

void send_remote_isupport(Client *client)
{
	int i;

	for (i = 0; ISupportStrings[i]; i++)
		sendnumeric(client, RPL_REMOTEISUPPORT, ISupportStrings[i]);
}

/** VERSION command:
 * Syntax: VERSION [server]
 */
CMD_FUNC(cmd_version)
{
	/* Only allow remote VERSIONs if registered -- Syzop */
	if (!IsUser(client) && !IsServer(client))
	{
		send_isupport(client);
		return;
	}

	if (hunt_server(client, recv_mtags, "VERSION", 1, parc, parv) == HUNTED_ISME)
	{
		sendnumeric(client, RPL_VERSION, version, debugmode, me.name,
			    (ValidatePermissionsForPath("server:info",client,NULL,NULL,NULL) ? serveropts : "0"),
			    extraflags ? extraflags : "",
			    tainted ? "3" : "",
			    (ValidatePermissionsForPath("server:info",client,NULL,NULL,NULL) ? MYOSNAME : "*"),
			    UnrealProtocol);
		if (ValidatePermissionsForPath("server:info",client,NULL,NULL,NULL))
		{
			sendnotice(client, "%s", SSLeay_version(SSLEAY_VERSION));
			sendnotice(client, "libsodium %s", sodium_version_string());
#ifdef USE_LIBCURL
			sendnotice(client, "%s", curl_version());
#endif
			sendnotice(client, "c-ares %s", ares_version(NULL));
			sendnotice(client, "%s", pcre2_version());
#if JANSSON_VERSION_HEX >= 0x020D00
			sendnotice(client, "jansson %s\n", jansson_version_str());
#endif
		}
		if (MyUser(client))
			send_isupport(client);
		else
			send_remote_isupport(client);
	}
}

char *num = NULL;

/** Send all our PROTOCTL messages to remote server.
 * We send multiple PROTOCTL's since 4.x. If this breaks your services
 * because you fail to maintain PROTOCTL state, then fix them!
 */
void send_proto(Client *client, ConfigItem_link *aconf)
{
	ISupport *prefix = ISupportFind("PREFIX");

	/* CAUTION: If adding a token to an existing PROTOCTL line below,
	 *          then ensure that MAXPARA is not reached!
	 */

	/* First line */
	sendto_one(client, NULL, "PROTOCTL NOQUIT NICKv2 SJOIN SJOIN2 UMODE2 VL SJ3 TKLEXT TKLEXT2 NICKIP ESVID NEXTBANS %s %s",
	           iConf.ban_setter_sync ? "SJSBY" : "",
	           ClientCapabilityFindReal("message-tags") ? "MTAGS" : "");

	/* Second line */
	sendto_one(client, NULL, "PROTOCTL CHANMODES=%s%s,%s,%s,%s USERMODES=%s BOOTED=%lld PREFIX=%s SID=%s MLOCK TS=%lld EXTSWHOIS",
		CHPAR1, EXPAR1, EXPAR2, EXPAR3, EXPAR4,
		umodestring, (long long)me.local->fake_lag, prefix->value,
		me.id, (long long)TStime());

	/* Third line */
	sendto_one(client, NULL, "PROTOCTL NICKCHARS=%s CHANNELCHARS=%s BIGLINES",
		charsys_get_current_languages(),
		allowed_channelchars_valtostr(iConf.allowed_channelchars));
}

#ifndef IRCDTOTALVERSION
#define IRCDTOTALVERSION BASE_VERSION "-" PATCH1 PATCH2 PATCH3 PATCH4 PATCH5 PATCH6 PATCH7 PATCH8 PATCH9
#endif

/** Special filter for remote commands */
int remotecmdfilter(Client *client, int parc, const char *parv[])
{
	/* no remote requests permitted from non-ircops */
	if (MyUser(client) && !ValidatePermissionsForPath("server:remote",client,NULL,NULL,NULL) && !BadPtr(parv[1]))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return 1; /* STOP */
	}

	/* same as above, but in case an old server forwards a request to us: we ignore it */
	if (!MyUser(client) && !ValidatePermissionsForPath("server:remote",client,NULL,NULL,NULL))
		return 1; /* STOP (return) */
	
	return 0; /* Continue */
}

/** Output for /INFO */
char *unrealinfo[] =
{
	"This release was brought to you by the following people:",
	"",
	"Head coder:",
	"* Bram Matthys (Syzop) <syzop@unrealircd.org>",
	"",
	"Coders:",
	"* Krzysztof Beresztant (k4be) <k4be@unrealircd.org>",
	"* Gottem <gottem@unrealircd.org>",
	"* i <i@unrealircd.org>",
	"",
	"Past UnrealIRCd 4.x coders/contributors:",
	"* Heero, binki, nenolod, ..",
	"",
	"Past UnrealIRCd 3.2.x coders/contributors:",
	"* Stskeeps (ret. head coder / project leader)",
	"* codemastr (ret. u3.2 head coder)",
	"* aquanight, WolfSage, ..",
	"* McSkaf, Zogg, NiQuiL, chasm, llthangel, nighthawk, ..",
	NULL
};

/** Send /INFO output */
void cmd_info_send(Client *client)
{
	char **text = unrealinfo;

	sendnumericfmt(client, RPL_INFO, ":========== %s ==========", IRCDTOTALVERSION);

	while (*text)
		sendnumericfmt(client, RPL_INFO, ":| %s", *text++);

	sendnumericfmt(client, RPL_INFO, ":|");
	sendnumericfmt(client, RPL_INFO, ":|");
	sendnumericfmt(client, RPL_INFO, ":| Credits - Type /CREDITS");
	sendnumericfmt(client, RPL_INFO, ":|");
	sendnumericfmt(client, RPL_INFO, ":| This is an UnrealIRCd-style server");
	sendnumericfmt(client, RPL_INFO, ":| If you find any bugs, please report them at:");
	sendnumericfmt(client, RPL_INFO, ":|  https://bugs.unrealircd.org/");
	sendnumericfmt(client, RPL_INFO, ":| UnrealIRCd Homepage: https://www.unrealircd.org");
	sendnumericfmt(client, RPL_INFO, ":============================================");
	sendnumericfmt(client, RPL_INFO, ":Birth Date: %s, compile # %s", creation, generation);
	sendnumericfmt(client, RPL_INFO, ":On-line since %s", myctime(me.local->creationtime));
	sendnumericfmt(client, RPL_INFO, ":ReleaseID (%s)", buildid);
	sendnumeric(client, RPL_ENDOFINFO);
}

/** The INFO command.
 * Syntax: INFO [server]
 */
CMD_FUNC(cmd_info)
{
	if (remotecmdfilter(client, parc, parv))
		return;

	if (hunt_server(client, recv_mtags, "INFO", 1, parc, parv) == HUNTED_ISME)
		cmd_info_send(client);
}

/** LICENSE command
 * Syntax: LICENSE [server]
 */
CMD_FUNC(cmd_license)
{
	char **text = gnulicense;

	if (remotecmdfilter(client, parc, parv))
		return;

	if (hunt_server(client, recv_mtags, "LICENSE", 1, parc, parv) == HUNTED_ISME)
	{
		while (*text)
			sendnumeric(client, RPL_INFO, *text++);

		sendnumeric(client, RPL_INFO, "");
		sendnumeric(client, RPL_ENDOFINFO);
	}
}

/** CREDITS command
 * Syntax: CREDITS [servername]
 */
CMD_FUNC(cmd_credits)
{
	char **text = unrealcredits;

	if (remotecmdfilter(client, parc, parv))
		return;

	if (hunt_server(client, recv_mtags, "CREDITS", 1, parc, parv) == HUNTED_ISME)
	{
		while (*text)
			sendnumeric(client, RPL_INFO, *text++);

		sendnumeric(client, RPL_INFO, "");
		sendnumericfmt(client, RPL_INFO, ":Birth Date: %s, compile # %s", creation, generation);
		sendnumericfmt(client, RPL_INFO, ":On-line since %s", myctime(me.local->creationtime));
		sendnumeric(client, RPL_ENDOFINFO);
	}
}

/** Return flags for a client (connection), eg 's' for TLS - used in STATS L/l */
const char *get_client_status(Client *client)
{
	static char buf[10];
	char *p = buf;

	*p = '\0';
	*p++ = '[';
	if (IsListening(client))
	{
		if (client->umodes & LISTENER_NORMAL)
			*p++ = '*';
		if (client->umodes & LISTENER_SERVERSONLY)
			*p++ = 'S';
		if (client->umodes & LISTENER_CLIENTSONLY)
			*p++ = 'C';
		if (client->umodes & LISTENER_TLS)
			*p++ = 's';
	}
	else
	{
		if (IsTLS(client))
			*p++ = 's';
	}
	*p++ = ']';
	*p++ = '\0';
	return buf;
}

/** ERROR command - used by servers to indicate errors.
 * Syntax: ERROR :<reason>
 */
CMD_FUNC(cmd_error)
{
	const char *para;

	if (!MyConnect(client))
		return;

	para = (parc > 1 && *parv[1] != '\0') ? parv[1] : "<>";

	/* Errors from untrusted sources are ignored as any
	 * malicious user can send these, confusing IRCOps etc.
	 * One can always see the errors from the other side anyway.
	 */
	if (!IsServer(client) && !client->server)
		return;

	unreal_log(ULOG_ERROR, "link", "LINK_ERROR_MESSAGE", client,
	           "Error from $client: $error_message",
	           log_data_string("error_message", para),
	           client->server->conf ? log_data_link_block(client->server->conf) : NULL);
}

/** Save the tunefile (such as: highest seen connection count) */
EVENT(save_tunefile)
{
	FILE *tunefile;

	tunefile = fopen(conf_files->tune_file, "w");
	if (!tunefile)
	{
		char *errstr = strerror(errno);
		unreal_log(ULOG_WARNING, "config", "WRITE_TUNE_FILE_FAILED", NULL,
		           "Unable to write tunefile '$filename': $system_error",
		           log_data_string("filename", conf_files->tune_file),
		           log_data_string("system_error", errstr));
		return;
	}
	fprintf(tunefile, "0\n");
	fprintf(tunefile, "%d\n", irccounts.me_max);
	fclose(tunefile);
}

/** Load the tunefile (such as: highest seen connection count) */
void load_tunefile(void)
{
	FILE *tunefile;
	char buf[1024];

	tunefile = fopen(conf_files->tune_file, "r");
	if (!tunefile)
		return;
	/* We ignore the first line, hence the weird looking double fgets here... */
	if (!fgets(buf, sizeof(buf), tunefile) || !fgets(buf, sizeof(buf), tunefile))
	{
		char *errstr = strerror(errno);
		unreal_log(ULOG_WARNING, "config", "READ_TUNE_FILE_FAILED", NULL,
		           "Unable to read tunefile '$filename': $system_error",
		           log_data_string("filename", conf_files->tune_file),
		           log_data_string("system_error", errstr));
	}
	irccounts.me_max = atol(buf);
	fclose(tunefile);
}

/** Rehash motd and rule files (motd_file/rules_file and all tld entries). */
void rehash_motdrules()
{
ConfigItem_tld *tlds;

	reread_motdsandrules();
	for (tlds = conf_tld; tlds; tlds = tlds->next)
	{
		/* read_motd() accepts NULL in first arg and acts sanely */
		read_motd(tlds->motd_file, &tlds->motd);
		read_motd(tlds->rules_file, &tlds->rules);
		read_motd(tlds->smotd_file, &tlds->smotd);
		read_motd(tlds->opermotd_file, &tlds->opermotd);
		read_motd(tlds->botmotd_file, &tlds->botmotd);
	}
}

/** Rehash motd and rules (only the default files) */
void reread_motdsandrules()
{
	read_motd(conf_files->motd_file, &motd);
	read_motd(conf_files->rules_file, &rules);
	read_motd(conf_files->smotd_file, &smotd);
	read_motd(conf_files->botmotd_file, &botmotd);
	read_motd(conf_files->opermotd_file, &opermotd);
	read_motd(conf_files->svsmotd_file, &svsmotd);
}

extern void reinit_resolver(Client *client);

/** REHASH command - reload configuration file on server(s).
 * Syntax: see HELPOP REHASH
 */
CMD_FUNC(cmd_rehash)
{
	int x;

	/* This is one of the (few) commands that cannot be handled
	 * by labeled-response accurately in all circumstances.
	 */
	labeled_response_inhibit = 1;

	if (!ValidatePermissionsForPath("server:rehash",client,NULL,NULL,NULL))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	if (!MyUser(client) && (parc < 2))
	{
		sendnumeric(client, ERR_NEEDMOREPARAMS, "REHASH");
		return;
	}

	if ((parc < 3) || BadPtr(parv[2])) {
		/* If the argument starts with a '-' (like -motd, -opermotd, etc) then it's
		 * assumed not to be a server. -- Syzop
		 */
		if (parv[1] && (parv[1][0] == '-'))
		{
			x = HUNTED_ISME;
		} else {
			if (!ValidatePermissionsForPath("server:rehash:global",client,NULL,NULL,NULL) &&
			    parv[1] && (find_client(parv[1], NULL) != &me))
			{
				sendnumeric(client, ERR_NOPRIVILEGES);
				return;
			}
			x = hunt_server(client, recv_mtags, "REHASH", 1, parc, parv);
		}
	} else {
		if (match_simple("-glob*", parv[1])) /* This is really ugly... hack to make /rehash -global -something work */
		{
			x = HUNTED_ISME;
		} else {
			if (!ValidatePermissionsForPath("server:rehash:global",client,NULL,NULL,NULL) &&
			    parv[1] && (find_client(parv[1], NULL) != &me))
			{
				sendnumeric(client, ERR_NOPRIVILEGES);
				return;
			}
			x = hunt_server(client, NULL, "REHASH", 1, parc, parv);
		}
	}
	if (x != HUNTED_ISME)
		return; /* Now forwarded or server didnt exist */

	if (!MyUser(client))
	{
#ifndef REMOTE_REHASH
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
#endif
		if (parv[2] == NULL)
		{
			if (loop.rehashing)
			{
				sendnotice(client, "A rehash is already in progress");
				return;
			}
			remote_rehash_client = client;
			/* fallthrough... so we deal with this the same way as local rehashes */
		}
		parv[1] = parv[2];
	} else {
		/* Ok this is in an 'else' because it should be only executed for local clients,
		 * but it's totally unrelated to the above ;).
		 */
		if (parv[1] && !strcasecmp(parv[1], "-all"))
		{
			sendnumeric(client, ERR_CANNOTDOCOMMAND, "REHASH",
			            "The command 'REHASH -all' does not exist. "
			            "Did you mean just 'REHASH'? "
			            "Or did you mean 'REHASH -global' which rehashes all IRC servers on the network?");
			/* In a future version we may make 'REHASH -all' to mean 'REHASH -global', but not yet.. */
			return;
		}
		if (parv[1] &&
		    (match_simple("-glob*", parv[1])
		     /* || (MyUser(client) && !strcasecmp(parv[1], "-all"))*/ ))
		{
			/* /REHASH -global [options] */
			Client *acptr;

			if (!ValidatePermissionsForPath("server:rehash:global",client,NULL,NULL,NULL))
			{
				sendnumeric(client, ERR_NOPRIVILEGES);
				return;
			}
			
			/* Shift parv's to the left */
			parv[1] = parv[2];
			parv[2] = NULL;
			parc--;
			if (parv[1] && *parv[1] != '-')
			{
				sendnotice(client, "You cannot specify a server name after /REHASH -global, for obvious reasons");
				return;
			}
			/* Broadcast it in an inefficient, but backwards compatible way. */
			list_for_each_entry(acptr, &global_server_list, client_node)
			{
				if (acptr == &me)
					continue;
				sendto_one(acptr, NULL, ":%s REHASH %s %s",
					client->name,
					acptr->name,
					parv[1] ? parv[1] : "");
			}
			/* Don't return, continue, because we need to REHASH ourselves as well. */
		}
	}

	if (!BadPtr(parv[1]) && strcasecmp(parv[1], "-all"))
	{
		if (*parv[1] == '-')
		{
			if (!strncasecmp("-gar", parv[1], 4))
			{
				loop.do_garbage_collect = 1;
				RunHook(HOOKTYPE_REHASHFLAG, client, parv[1]);
				return;
			}
			if (!strncasecmp("-dns", parv[1], 4))
			{
				reinit_resolver(client);
				return;
			}
			if (match_simple("-ssl*", parv[1]) || match_simple("-tls*", parv[1]))
			{
				unreal_log(ULOG_INFO, "config", "CONFIG_RELOAD_TLS", client, "Reloading all TLS related data. [by: $client.details]");
				reinit_tls();
				return;
			}
			if (!strcmp("-centralspamfilter", parv[1]) || !strcmp("-cs", parv[1]))
			{
				if (!iConf.central_spamfilter_enabled)
				{
					sendnotice(client, "ERROR: Central Spamfilter is not enabled on this server.");
				} else {
					unreal_log(ULOG_INFO, "central-spamfilter", "CENTRAL_SPAMFILTER_RELOAD", client,
					           "Reloading Central Spamfilter rules. [by: $client.details]");
					central_spamfilter_last_download = 0;
				}
				return;
			}
			RunHook(HOOKTYPE_REHASHFLAG, client, parv[1]);
			return;
		}
	}
	else
	{
		if (loop.rehashing)
		{
			sendnotice(client, "ERROR: A rehash is already in progress");
			return;
		}
		unreal_log(ULOG_INFO, "config", "CONFIG_RELOAD", client, "Rehashing server configuration file [by: $client.details]");
	}

	/* Normal rehash, rehash motds&rules too, just like the on in the tld block will :p */
	sendnumeric(client, RPL_REHASHING, configfile);
	request_rehash(client);
}

/** RESTART command - restart the server (discouraged command)
 * parv[1] - password *OR* reason if no drpass { } block exists
 * parv[2] - reason for restart (optional & only if drpass block exists)
 */
CMD_FUNC(cmd_restart)
{
	const char *reason = parv[1];
	Client *acptr;

	if (!MyUser(client))
		return;

	/* Check permissions */
	if (!ValidatePermissionsForPath("server:restart",client,NULL,NULL,NULL))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	/* Syntax: /restart */
	if (parc == 1)
	{
		if (conf_drpass)
		{
			sendnumeric(client, ERR_NEEDMOREPARAMS, "RESTART");
			return;
		}
	} else
	if (parc >= 2)
	{
		/* Syntax: /restart <pass> [reason] */
		if (conf_drpass)
		{
			if (!Auth_Check(client, conf_drpass->restartauth, parv[1]))
			{
				sendnumeric(client, ERR_PASSWDMISMATCH);
				return;
			}
			reason = parv[2];
		}
	}

	list_for_each_entry(acptr, &lclient_list, lclient_node)
	{
		if (IsUser(acptr))
			sendnotice(acptr, "Server Restarted by %s", client->name);
		else if (IsServer(acptr))
			sendto_one(acptr, NULL, ":%s ERROR :Restarted by %s: %s",
			    me.name, get_client_name(client, TRUE), reason ? reason : "No reason");
	}

	server_reboot(reason ? reason : "No reason");
}

/** Send short message of the day to the client */
void short_motd(Client *client)
{
	ConfigItem_tld *tld;
	MOTDFile *themotd;
	MOTDLine *motdline;
	struct tm *tm;
	char is_short;

	tm = NULL;
	is_short = 1;

	tld = find_tld(client);

	/*
	* Try different sources of short MOTDs, falling back to the
	* long MOTD.
	*/
	themotd = &smotd;
	if (tld && tld->smotd.lines)
		themotd = &tld->smotd;

	/* try long MOTDs */
	if (!themotd->lines)
	{
		is_short = 0;
		if (tld && tld->motd.lines)
			themotd = &tld->motd;
		else
			themotd = &motd;
	}

	if (!themotd->lines)
	{
		sendnumeric(client, ERR_NOMOTD);
		return;
	}
	if (themotd->last_modified.tm_year)
	{
		tm = &themotd->last_modified; /* for readability */
		sendnumeric(client, RPL_MOTDSTART, me.name);
		sendnumericfmt(client, RPL_MOTD, ":- %d/%d/%d %d:%02d", tm->tm_mday, tm->tm_mon + 1,
		               1900 + tm->tm_year, tm->tm_hour, tm->tm_min);
	}
	if (is_short)
	{
		sendnumeric(client, RPL_MOTD, "This is the short MOTD. To view the complete MOTD type /motd");
		sendnumeric(client, RPL_MOTD, "");
	}

	motdline = themotd->lines;
	while (motdline)
	{
		sendnumeric(client, RPL_MOTD, motdline->line);
		motdline = motdline->next;
	}

	if (!is_short)
	{
		/* If the admin does not use a short MOTD then we append the SVSMOTD here...
		 * If we did show a short motd then we don't append SVSMOTD,
		 * since they want to keep it short.
		 */
		motdline = svsmotd.lines;
		while (motdline)
		{
			sendnumeric(client, RPL_MOTD, motdline->line);
			motdline = motdline->next;
		}
	}

	sendnumeric(client, RPL_ENDOFMOTD);
}

/** Read motd-like file, used for rules/motd/botmotd/opermotd/etc.
 * @param filename Filename of file to read or URL. NULL is accepted and causes the *motd to be free()d.
 * @param motd Reference to motd pointer (used for freeing if needed and for asynchronous remote MOTD support)
 */
void read_motd(const char *filename, MOTDFile *themotd)
{
	FILE *fd;
	struct tm *tm_tmp;
	time_t modtime;

	char line[512];
	char *tmp;

	MOTDLine *last, *temp;

	free_motd(themotd);

	if (!filename)
		return;

	fd = fopen(filename, "r");
	if (!fd)
		return;

	/* record file modification time */
	modtime = unreal_getfilemodtime(filename);
	tm_tmp = localtime(&modtime);
	memcpy(&themotd->last_modified, tm_tmp, sizeof(struct tm));

	last = NULL;
	while (fgets(line, sizeof(line), fd))
	{
		if ((tmp = strchr(line, '\n')))
			*tmp = '\0';
		if ((tmp = strchr(line, '\r')))
			*tmp = '\0';
		
		if (strlen(line) > 510)
			line[510] = '\0';

		temp = safe_alloc(sizeof(MOTDLine));
		safe_strdup(temp->line, line);

		if (last)
			last->next = temp;
		else
			/* handle the special case of the first line */
			themotd->lines = temp;

		last = temp;
	}
	/* the file could be zero bytes long? */
	if (last)
		last->next = NULL;

	fclose(fd);
	
	return;
}

/** Free the contents of a MOTDFile structure.
 * The MOTDFile structure itself should be statically
 * allocated and deallocated. If the caller wants, it must
 * manually free the MOTDFile structure itself.
 */
void free_motd(MOTDFile *themotd)
{
	MOTDLine *next, *motdline;

	if (!themotd)
		return;

	for (motdline = themotd->lines; motdline; motdline = next)
	{
		next = motdline->next;
		safe_free(motdline->line);
		safe_free(motdline);
	}

	themotd->lines = NULL;
	memset(&themotd->last_modified, '\0', sizeof(struct tm));
}

/** DIE command - terminate the server
 * DIE [password]
 */
CMD_FUNC(cmd_die)
{
	Client *acptr;

	if (!MyUser(client))
		return;

	if (!ValidatePermissionsForPath("server:die",client,NULL,NULL,NULL))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	if (conf_drpass)	/* See if we have and DIE/RESTART password */
	{
		if (parc < 2)	/* And if so, require a password :) */
		{
			sendnumeric(client, ERR_NEEDMOREPARAMS, "DIE");
			return;
		}
		if (!Auth_Check(client, conf_drpass->dieauth, parv[1]))
		{
			sendnumeric(client, ERR_PASSWDMISMATCH);
			return;
		}
	}

	/* Let the +s know what is going on */
	unreal_log(ULOG_INFO, "main", "UNREALIRCD_STOP", client,
	           "Terminating server by request of $client.details");

	list_for_each_entry(acptr, &lclient_list, lclient_node)
	{
		if (IsUser(acptr))
			sendnotice(acptr, "Server Terminated by %s", 
				client->name);
		else if (IsServer(acptr))
			sendto_one(acptr, NULL, ":%s ERROR :Terminated by %s",
			    me.name, get_client_name(client, TRUE));
	}

	s_die();
}

/** Server list (network) of pending connections */
PendingNet *pendingnet = NULL;

/** Add server list (network) from 'client' connection */
void add_pending_net(Client *client, const char *str)
{
	PendingNet *net;
	PendingServer *srv;
	char *p, *name;
	char buf[512];

	if (BadPtr(str) || !client)
		return;

	/* Skip any * at the beginning (indicating a reply),
	 * and work on a copy.
	 */
	if (*str == '*')
		strlcpy(buf, str+1, sizeof(buf));
	else
		strlcpy(buf, str, sizeof(buf));

	/* Allocate */
	net = safe_alloc(sizeof(PendingNet));
	net->client = client;

	for (name = strtoken(&p, buf, ","); name; name = strtoken(&p, NULL, ","))
	{
		if (!*name)
			continue;
		
		srv = safe_alloc(sizeof(PendingServer));
		strlcpy(srv->sid, name, sizeof(srv->sid));
		AddListItem(srv, net->servers);
	}
	
	AddListItem(net, pendingnet);
}

/** Free server list (network) previously added by 'client' */
void free_pending_net(Client *client)
{
	PendingNet *net, *net_next;
	PendingServer *srv, *srv_next;
	
	for (net = pendingnet; net; net = net_next)
	{
		net_next = net->next;
		if (net->client == client)
		{
			for (srv = net->servers; srv; srv = srv_next)
			{
				srv_next = srv->next;
				safe_free(srv);
			}
			DelListItem(net, pendingnet);
			safe_free(net);
			/* Don't break, there can be multiple objects */
		}
	}
}

/** Find SID in any server list (network) that is pending, except 'exempt' */
PendingNet *find_pending_net_by_sid_butone(const char *sid, Client *exempt)
{
	PendingNet *net;
	PendingServer *srv;

	if (BadPtr(sid))
		return NULL;

	for (net = pendingnet; net; net = net->next)
	{
		if (net->client == exempt)
			continue;
		for (srv = net->servers; srv; srv = srv->next)
			if (!strcmp(srv->sid, sid))
				return net;
	}
	return NULL;
}

/** Search the pending connections list for any identical sids */
Client *find_pending_net_duplicates(Client *cptr, Client **srv, char **sid)
{
	PendingNet *net, *other;
	PendingServer *s;

	*srv = NULL;
	*sid = NULL;
	
	for (net = pendingnet; net; net = net->next)
	{
		if (net->client != cptr)
			continue;
		/* Ok, found myself */
		for (s = net->servers; s; s = s->next)
		{
			char *curr_sid = s->sid;
			other = find_pending_net_by_sid_butone(curr_sid, cptr);
			if (other)
			{
				*srv = net->client;
				*sid = s->sid;
				return other->client; /* Found another (pending) server with identical numeric */
			}
		}
	}
	
	return NULL;
}

/** Like find_pending_net_duplicates() but the other way around? Eh.. */
Client *find_non_pending_net_duplicates(Client *client)
{
	PendingNet *net;
	PendingServer *s;
	Client *acptr;

	for (net = pendingnet; net; net = net->next)
	{
		if (net->client != client)
			continue;
		/* Ok, found myself */
		for (s = net->servers; s; s = s->next)
		{
			acptr = find_server(s->sid, NULL);
			if (acptr)
				return acptr; /* Found another (fully CONNECTED) server with identical numeric */
		}
	}
	
	return NULL;
}

/** Parse CHANMODES= in PROTOCTL */
void parse_chanmodes_protoctl(Client *client, const char *str)
{
	char *modes, *p;
	char copy[256];

	strlcpy(copy, str, sizeof(copy));

	modes = strtoken(&p, copy, ",");
	if (modes)
	{
		safe_strdup(client->server->features.chanmodes[0], modes);
		modes = strtoken(&p, NULL, ",");
		if (modes)
		{
			safe_strdup(client->server->features.chanmodes[1], modes);
			modes = strtoken(&p, NULL, ",");
			if (modes)
			{
				safe_strdup(client->server->features.chanmodes[2], modes);
				modes = strtoken(&p, NULL, ",");
				if (modes)
				{
					safe_strdup(client->server->features.chanmodes[3], modes);
				}
			}
		}
	}
}

static char previous_langsinuse[512];
static int previous_langsinuse_ready = 0;

/** Check the nick character system (set::allowed-nickchars) for changes.
 * If there are changes, then we broadcast the new PROTOCTL NICKCHARS= to all servers.
 */
void charsys_check_for_changes(void)
{
	const char *langsinuse = charsys_get_current_languages();
	/* already called by charsys_finish() */
	safe_strdup(me.server->features.nickchars, langsinuse);

	if (!previous_langsinuse_ready)
	{
		previous_langsinuse_ready = 1;
		strlcpy(previous_langsinuse, langsinuse, sizeof(previous_langsinuse));
		return; /* not booted yet. then we are done here. */
	}

	if (strcmp(langsinuse, previous_langsinuse))
	{
		unreal_log(ULOG_INFO, "charsys", "NICKCHARS_CHANGED", NULL,
		           "Permitted nick characters changed at runtime: $old_nickchars -> $new_nickchars",
		           log_data_string("old_nickchars", previous_langsinuse),
		           log_data_string("new_nickchars", langsinuse));
		/* Broadcast change to all (locally connected) servers */
		sendto_server(NULL, 0, 0, NULL, "PROTOCTL NICKCHARS=%s", langsinuse);
	}

	strlcpy(previous_langsinuse, langsinuse, sizeof(previous_langsinuse));
}

/** Check if supplied server name is valid, that is: does not contain forbidden characters etc */
int valid_server_name(const char *name)
{
	const char *p;

	if (!valid_host(name, 0))
		return 0; /* invalid hostname */

	if (!strchr(name, '.'))
		return 0; /* no dot */

	return 1;
}

/** Check if the supplied name is a valid SID, as in: syntax. */
int valid_sid(const char *name)
{
	if (strlen(name) != 3)
		return 0;
	if (!isdigit(*name))
		return 0;
	if (!isdigit(name[1]) && !isupper(name[1]))
		return 0;
	if (!isdigit(name[2]) && !isupper(name[2]))
		return 0;
	return 1;
}

/** Check if the supplied name is a valid UID, as in: syntax. */
int valid_uid(const char *name)
{
	const char *p;

	/* Enforce at least some minimum length */
	if (strlen(name) < 6)
		return 0;

	/* UID cannot be larger than IDLEN or it would be cut off later */
	if (strlen(name) > IDLEN)
		return 0;

	/* Must start with a digit */
	if (!isdigit(*name))
		return 0;

	/* For all the remaining characters: digit or uppercase character */
	for (p = name+1; *p; p++)
		if (!isdigit(*p) && !isupper(*p))
			return 0;

	return 1;
}

/** Initialize the TKL subsystem */
void tkl_init(void)
{
	memset(tklines, 0, sizeof(tklines));
	memset(tklines_ip_hash, 0, sizeof(tklines_ip_hash));
}

/** Called when a server link is lost.
 * Used for logging only, API users can use the HOOKTYPE_SERVER_QUIT hook.
 */
void lost_server_link(Client *client, const char *tls_error_string)
{
	if (IsServer(client))
	{
		/* An already established link is now lost. */
		// FIXME: we used to broadcast this GLOBALLY.. not anymore since the U6 rewrite.. is that what we want?
		if (tls_error_string)
		{
			/* TLS */
			unreal_log(ULOG_ERROR, "link", "LINK_DISCONNECTED", client,
				   "Lost server link to $client [$client.ip]: $tls_error_string",
				   log_data_string("tls_error_string", tls_error_string),
				   client->server->conf ? log_data_link_block(client->server->conf) : NULL);
		} else {
			/* NON-TLS */
			unreal_log(ULOG_ERROR, "link", "LINK_DISCONNECTED", client,
				   "Lost server link to $client [$client.ip]: $socket_error",
				   log_data_socket_error(client->local->fd),
				   client->server->conf ? log_data_link_block(client->server->conf) : NULL);
		}
	} else {
		/* A link attempt failed (it was never a fully connected server) */
		/* We send these to local ops only */
		if (tls_error_string)
		{
			/* TLS */
			if (client->server->conf)
			{
				unreal_log(ULOG_ERROR, "link", "LINK_ERROR_CONNECT", client,
					   client->server->conf->outgoing.file
					   ? "Unable to link with server $client [$link_block.file]: $tls_error_string"
					   : "Unable to link with server $client [$link_block.ip:$link_block.port]: $tls_error_string",
					   log_data_string("tls_error_string", tls_error_string),
					   log_data_link_block(client->server->conf));
			} else {
				unreal_log(ULOG_ERROR, "link", "LINK_ERROR_CONNECT", client,
					   "Unable to link with server $client: $tls_error_string",
					   log_data_string("tls_error_string", tls_error_string));
			}
		} else {
			/* non-TLS */
			if (client->server->conf)
			{
				unreal_log(ULOG_ERROR, "link", "LINK_ERROR_CONNECT", client,
					   client->server->conf->outgoing.file
					   ? "Unable to link with server $client [$link_block.file]: $socket_error"
					   : "Unable to link with server $client [$link_block.ip:$link_block.port]: $socket_error",
					   log_data_socket_error(client->local->fd),
					   log_data_link_block(client->server->conf));
			} else {
				unreal_log(ULOG_ERROR, "link", "LINK_ERROR_CONNECT", client,
					   "Unable to link with server $client: $socket_error",
					   log_data_socket_error(client->local->fd));
			}
		}
	}
	SetServerDisconnectLogged(client);
}

/** Reject an insecure (outgoing) server link that isn't TLS.
 * This function is void and not int because it can be called from other void functions
 */
void reject_insecure_server(Client *client)
{
	unreal_log(ULOG_ERROR, "link", "SERVER_STARTTLS_FAILED", client,
	           "Could not link with server $client with TLS enabled. "
	           "Please check logs on the other side of the link. "
	           "If you insist with insecure linking then you can set link::options::outgoing::insecure "
	           "(NOT recommended!).");
	dead_socket(client, "Rejected server link without TLS");
}

/** Start server handshake - called after the outgoing connection has been established.
 * @param client	The remote server
 */
void start_server_handshake(Client *client)
{
	ConfigItem_link *aconf = client->server ? client->server->conf : NULL;

	if (!aconf)
	{
		/* Should be impossible. */
		unreal_log(ULOG_ERROR, "link", "BUG_LOST_CONFIGURATION_ON_HANDSHAKE", client,
		           "Lost configuration while connecting to $client.details");
		return;
	}

	RunHook(HOOKTYPE_SERVER_HANDSHAKE_OUT, client);

	sendto_one(client, NULL, "PASS :%s", (aconf->auth->type == AUTHTYPE_PLAINTEXT) ? aconf->auth->data : "*");

	send_protoctl_servers(client, 0);
	send_proto(client, aconf);
	/* Sending SERVER message moved to cmd_protoctl, so it's send after the first PROTOCTL
	 * that we receive from the remote server. -- Syzop
	 */
}
