/*
 *   IRC - Internet Relay Chat, src/modules/protoctl.c
 *   (C) 2004- 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"

CMD_FUNC(cmd_protoctl);

#define MSG_PROTOCTL 	"PROTOCTL"	

ModuleHeader MOD_HEADER
  = {
	"protoctl",
	"5.0",
	"command /protoctl", 
	"UnrealIRCd Team",
	"unrealircd-6",
    };

MOD_INIT()
{
	CommandAdd(modinfo->handle, MSG_PROTOCTL, cmd_protoctl, MAXPARA, CMD_UNREGISTERED|CMD_SERVER|CMD_USER);
	MARK_AS_OFFICIAL_MODULE(modinfo);
	return MOD_SUCCESS;
}

MOD_LOAD()
{
	return MOD_SUCCESS;
}

MOD_UNLOAD()
{
	return MOD_SUCCESS;
}

#define MAX_SERVER_TIME_OFFSET 60

/* The PROTOCTL command is used for negotiating capabilities with
 * directly connected servers.
 * See https://www.unrealircd.org/docs/Server_protocol:PROTOCTL_command
 * for all technical documentation, especially if you are a server
 * or services coder.
 */
CMD_FUNC(cmd_protoctl)
{
	int  i;
	int first_protoctl = IsProtoctlReceived(client) ? 0 : 1; /**< First PROTOCTL we receive? Special ;) */
	char proto[512];
	char *name, *value, *p;

	if (!MyConnect(client))
		return; /* Remote PROTOCTL's are not supported */

	SetProtoctlReceived(client);

	for (i = 1; i < parc; i++)
	{
		strlcpy(proto, parv[i], sizeof proto);
		p = strchr(proto, '=');
		if (p)
		{
			name = proto;
			*p++ = '\0';
			value = p;
		} else {
			name = proto;
			value = NULL;
		}

		if (!strcmp(name, "NAMESX"))
		{
			SetCapability(client, "multi-prefix");
		}
		else if (!strcmp(name, "UHNAMES") && UHNAMES_ENABLED)
		{
			SetCapability(client, "userhost-in-names");
		}
		else if (IsUser(client))
		{
			return;
		}
		else if (!strcmp(name, "VL"))
		{
			SetVL(client);
		}
		else if (!strcmp(name, "VHP"))
		{
			SetVHP(client);
		}
		else if (!strcmp(name, "CLK"))
		{
			SetCLK(client);
		}
		else if (!strcmp(name, "SJSBY") && iConf.ban_setter_sync)
		{
			SetSJSBY(client);
		}
		else if (!strcmp(name, "MTAGS"))
		{
			SetMTAGS(client);
		}
		else if (!strcmp(name, "NEXTBANS"))
		{
			SetNEXTBANS(client);
		}
		else if (!strcmp(name, "BIGLINES"))
		{
			SetBIGLINES(client);
		}
		else if (!strcmp(name, "NICKCHARS") && value)
		{
			if (!IsServer(client) && !IsEAuth(client) && !IsHandshake(client))
				continue;
			/* Ok, server is either authenticated, or is an outgoing connect... */
			/* Some combinations are fatal because they would lead to mass-kills:
			 * - use of 'utf8' on our server but not on theirs
			 */
			if (strstr(charsys_get_current_languages(), "utf8") && !strstr(value, "utf8"))
			{
				unreal_log(ULOG_ERROR, "link", "LINK_DENIED_CHARSYS_INCOMPATIBLE", client,
					   "Server link $client rejected. Server $me_name has utf8 in set::allowed-nickchars but $client does not.",
					   log_data_string("me_name", me.name));
				exit_client(client, NULL, "Incompatible set::allowed-nickchars setting");
				return;
			}
			/* We compare the character sets to see if we should warn opers about any mismatch... */
			if (strcmp(value, charsys_get_current_languages()))
			{
				unreal_log(ULOG_WARNING, "link", "LINK_WARNING_CHARSYS", client,
					   "Server link $client does not have the same set::allowed-nickchars settings, "
					   "this may possibly cause display issues. Our charset: '$our_charsys', theirs: '$their_charsys'",
					   log_data_string("our_charsys", charsys_get_current_languages()),
					   log_data_string("their_charsys", value));
			}
			if (client->server)
				safe_strdup(client->server->features.nickchars, value);

			/* If this is a runtime change (so post-handshake): */
			if (IsServer(client))
				broadcast_sinfo(client, NULL, client);
		}
		else if (!strcmp(name, "CHANNELCHARS") && value)
		{
			int their_value;

			if (!IsServer(client) && !IsEAuth(client) && !IsHandshake(client))
				continue;

			their_value = allowed_channelchars_strtoval(value);
			if (their_value != iConf.allowed_channelchars)
			{
				unreal_log(ULOG_ERROR, "link", "LINK_DENIED_ALLOWED_CHANNELCHARS_INCOMPATIBLE", client,
					   "Server link $client rejected. Server has set::allowed-channelchars setting "
					   "of $their_allowed_channelchars, while we have $our_allowed_channelchars.\n"
					   "Please set set::allowed-channelchars to the same value on all servers.",
					   log_data_string("their_allowed_channelchars", value),
					   log_data_string("our_allowed_channelchars", allowed_channelchars_valtostr(iConf.allowed_channelchars)));
				exit_client(client, NULL, "Incompatible set::allowed-channelchars setting");
				return;
			}
		}
		else if (!strcmp(name, "SID") && value)
		{
			Client *aclient;
			char *sid = value;

			if (!IsServer(client) && !IsEAuth(client) && !IsHandshake(client))
			{
				exit_client(client, NULL, "Got PROTOCTL SID before EAUTH, that's the wrong order!");
				return;
			}

			if (*client->id && (strlen(client->id)==3))
			{
				exit_client(client, NULL, "Got PROTOCTL SID twice");
				return;
			}

			if (!valid_sid(value))
			{
				exit_client(client, NULL, "Invalid SID. The first character must be a digit and the other two characters must be A-Z0-9. Eg: 0AA.");
				return;
			}

			if (IsServer(client))
			{
				exit_client(client, NULL, "Got PROTOCTL SID after SERVER, that's the wrong order!");
				return;
			}

			if ((aclient = hash_find_id(sid, NULL)) != NULL)
			{
				unreal_log(ULOG_ERROR, "link", "LINK_DENIED_SID_COLLISION", client,
					   "Server link $client rejected. Server with SID $sid already exist via uplink $existing_client.server.uplink.",
					   log_data_string("sid", sid),
					   log_data_client("existing_client", aclient));
				exit_client(client, NULL, "SID collision");
				return;
			}

			if (*client->id)
				del_from_id_hash_table(client->id, client); /* delete old UID entry (created on connect) */
			strlcpy(client->id, sid, sizeof(client->id));
			add_to_id_hash_table(client->id, client); /* add SID */
		}
		else if (!strcmp(name, "EAUTH") && value)
		{
			/* Early authorization: EAUTH=servername,protocol,flags,software
			 * (Only servername is mandatory, rest is optional)
			 */
			int ret;
			char *p;
			char *servername = NULL, *protocol = NULL, *flags = NULL, *software = NULL;
			char buf[512];
			ConfigItem_link *aconf = NULL;

			if (IsEAuth(client))
			{
				exit_client(client, NULL, "PROTOCTL EAUTH received twice");
				return;
			}

			strlcpy(buf, value, sizeof(buf));
			p = strchr(buf, ' ');
			if (p)
			{
				*p = '\0';
				p = NULL;
			}
			
			servername = strtoken_noskip(&p, buf, ",");
			if (!servername || !valid_server_name(servername))
			{
				exit_client(client, NULL, "Bogus server name");
				return;
			}
			
			
			protocol = strtoken_noskip(&p, NULL, ",");
			if (protocol)
			{
				flags = strtoken_noskip(&p, NULL, ",");
				if (flags)
					software = strtoken_noskip(&p, NULL, ",");
			}
			
			/* Set client->name but don't add to hash list, this gives better
			 * log messages and should be safe. See CMTSRV941 in server.c.
			 */
			strlcpy(client->name, servername, sizeof(client->name));

			if (!(aconf = verify_link(client)))
				return;

			/* note: software, protocol and flags may be NULL */
			if (!check_deny_version(client, software, protocol ? atoi(protocol) : 0, flags))
				return;

			SetEAuth(client);
			make_server(client); /* allocate and set client->server */
			if (protocol)
				client->server->features.protocol = atoi(protocol);
			if (software)
				safe_strdup(client->server->features.software, software);
			if (is_services_but_not_ulined(client))
			{
				exit_client_fmt(client, NULL, "Services detected but no ulines { } for server name %s", client->name);
				return;
			}
			if (!IsHandshake(client) && aconf) /* Send PASS early... */
				sendto_one(client, NULL, "PASS :%s", (aconf->auth->type == AUTHTYPE_PLAINTEXT) ? aconf->auth->data : "*");
		}
		else if (!strcmp(name, "SERVERS") && value)
		{
			Client *aclient, *srv;
			char *sid = NULL;
			
			if (!IsEAuth(client))
				continue;
				
			if (client->server->features.protocol < 2351)
				continue; /* old SERVERS= version */
			
			/* Other side lets us know which servers are behind it.
			 * SERVERS=<sid-of-server-1>[,<sid-of-server-2[,..etc..]]
			 * Eg: SERVERS=001,002,0AB,004,005
			 */

			add_pending_net(client, value);

			aclient = find_non_pending_net_duplicates(client);
			if (aclient)
			{
				unreal_log(ULOG_ERROR, "link", "LINK_DENIED_DUPLICATE_SID", client,
					   "Denied server $client: Server with SID $existing_client.id ($existing_client) is already linked.",
					   log_data_client("existing_client", aclient));
				exit_client(client, NULL, "Server Exists (or non-unique me::sid)");
				return;
			}
			
			aclient = find_pending_net_duplicates(client, &srv, &sid);
			if (aclient)
			{
				unreal_log(ULOG_ERROR, "link", "LINK_DENIED_DUPLICATE_SID_LINKED", client,
					   "Denied server $client: Server would (later) introduce SID $sid, "
					   "but we already have SID $sid linked ($existing_client)\n"
					   "Possible race condition, just wait a moment for the network to synchronize...",
					   log_data_string("sid", sid),
					   log_data_client("existing_client", aclient));
				exit_client(client, NULL, "Server Exists (just wait a moment...)");
				return;
			}

			/* Send our PROTOCTL SERVERS= back if this was NOT a response */
			if (*value != '*')
				send_protoctl_servers(client, 1);
		}
		else if (!strcmp(name, "TS") && value && (IsServer(client) || IsEAuth(client)))
		{
			long t = atol(value);

			if (t < 10000)
				continue; /* ignore */

			if ((TStime() - t) > MAX_SERVER_TIME_OFFSET)
			{
				unreal_log(ULOG_ERROR, "link", "LINK_DENIED_CLOCK_INCORRECT", client,
				           "Denied server $client: clock on server $client is $time_delta "
				           "seconds behind the clock of $me_name.\n"
				           "Correct time is very important for IRC servers, "
				           "see https://www.unrealircd.org/docs/FAQ#fix-your-clock",
				           log_data_integer("time_delta", TStime() - t),
				           log_data_string("me_name", me.name));
				exit_client_fmt(client, NULL, "Incorrect clock. Our clocks are %lld seconds apart.",
				                (long long)(TStime() - t));
				return;
			} else
			if ((t - TStime()) > MAX_SERVER_TIME_OFFSET)
			{
				unreal_log(ULOG_ERROR, "link", "LINK_DENIED_CLOCK_INCORRECT", client,
				           "Denied server $client: clock on server $client is $time_delta "
				           "seconds ahead the clock of $me_name.\n"
				           "Correct time is very important for IRC servers, "
				           "see https://www.unrealircd.org/docs/FAQ#fix-your-clock",
				           log_data_integer("time_delta", t - TStime()),
				           log_data_string("me_name", me.name));
				exit_client_fmt(client, NULL, "Incorrect clock. Our clocks are %lld seconds apart.",
				                (long long)(t - TStime()));
				return;
			}
		}
		else if (!strcmp(name, "MLOCK"))
		{
			client->local->proto |= PROTO_MLOCK;
		}
		else if (!strcmp(name, "CHANMODES") && value && client->server)
		{
			parse_chanmodes_protoctl(client, value);
			/* If this is a runtime change (so post-handshake): */
			if (IsServer(client))
				broadcast_sinfo(client, NULL, client);
		}
		else if (!strcmp(name, "USERMODES") && value && client->server)
		{
			safe_strdup(client->server->features.usermodes, value);
			/* If this is a runtime change (so post-handshake): */
			if (IsServer(client))
				broadcast_sinfo(client, NULL, client);
		}
		else if (!strcmp(name, "BOOTED") && value && client->server)
		{
			client->server->boottime = atol(value);
		}
		else if (!strcmp(name, "EXTSWHOIS"))
		{
			client->local->proto |= PROTO_EXTSWHOIS;
		}
		/* You can add protocol extensions here.
		 * Use 'name' and 'value' (the latter may be NULL).
		 *
		 * DO NOT error or warn on unknown proto; we just don't
		 * support it.
		 */
	}

	if (first_protoctl && IsHandshake(client) && client->server && !IsServerSent(client)) /* first & outgoing connection to server */
	{
		/* SERVER message moved from completed_connection() to here due to EAUTH/SERVERS PROTOCTL stuff,
		 * which needed to be delayed until after both sides have received SERVERS=xx (..or not.. in case
		 * of older servers).
		 */
		send_server_message(client);
	}
}
