From 8d05bf669b80e0caddf000ffb065bf4672ed1fd4 Mon Sep 17 00:00:00 2001
From: toaster <rollerorbital@gmail.com>
Date: Tue, 14 Jun 2022 17:15:28 +0100
Subject: [PATCH] Kicks are now temp bans

Length is determined by the "kicktime" cvar, in minutes. By default, this is set to 10, but I'm willing to adjust this. Only applies to manual kicks (in the future, maybe also name filter kicks).

The timestamp for the unban time is even saved in ban.txt, so long-term temporary bans are completely possible. (I checked, you can attempt to ban someone for up to 1902 years if you really want to.)

# Conflicts:
#	src/d_clisrv.c
#	src/d_clisrv.h
#	src/i_tcp.c
---
 src/d_clisrv.c | 121 +++++++++++++++++++++++++++++++++++++++++++------
 src/d_clisrv.h |   2 +
 src/d_net.c    |   3 ++
 src/i_net.h    |   5 ++
 src/i_tcp.c    |  64 ++++++++++++++++++++++++--
 5 files changed, 175 insertions(+), 20 deletions(-)

diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 65f3e8b79..ec2ce776c 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -180,6 +180,8 @@ consvar_t cv_playbackspeed = {"playbackspeed", "1", 0, playbackspeed_cons_t, NUL
 
 consvar_t cv_httpsource = {"http_source", "", CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
 
+consvar_t cv_kicktime = CVAR_INIT ("kicktime", "10", CV_SAVE, CV_Unsigned, NULL);
+
 static inline void *G_DcpyTiccmd(void* dest, const ticcmd_t* src, const size_t n)
 {
 	const size_t d = n / sizeof(ticcmd_t);
@@ -2654,6 +2656,13 @@ static void Command_ShowBan(void) //Print out ban list
 		else
 			CONS_Printf("%s: %s/%s ", sizeu1(i+1), address, mask);
 
+		if (I_GetUnbanTime && I_GetUnbanTime(i) != NO_BAN_TIME)
+		{
+			// todo: maybe try to actually print out the time remaining,
+			// and/or the datetime of the unbanning?
+			CONS_Printf("(temporary) ");
+		}
+
 		if (I_GetBanReason && (reason = I_GetBanReason(i)) != NULL)
 			CONS_Printf("(%s)\n", reason);
 		else
@@ -2669,6 +2678,8 @@ void D_SaveBan(void)
 	FILE *f;
 	size_t i;
 	const char *address, *mask, *reason;
+	const time_t curTime = time(NULL);
+	time_t unbanTime = NO_BAN_TIME;
 	const char *path = va("%s"PATHSEP"%s", srb2home, "ban.txt");
 
 	f = fopen(path, "w");
@@ -2681,11 +2692,24 @@ void D_SaveBan(void)
 
 	for (i = 0; (address = I_GetBanAddress(i)) != NULL; i++)
 	{
+		if (I_GetUnbanTime)
+		{
+			unbanTime = I_GetUnbanTime(i);
+		}
+
+		if (unbanTime != NO_BAN_TIME && curTime >= unbanTime)
+		{
+			// Don't need to save this one anymore.
+			continue;
+		}
+
 		if (!I_GetBanMask || (mask = I_GetBanMask(i)) == NULL)
 			fprintf(f, "%s 0", address);
 		else
 			fprintf(f, "%s %s", address, mask);
 
+		fprintf(f, " %ld", (long)unbanTime);
+
 		if (I_GetBanReason && (reason = I_GetBanReason(i)) != NULL)
 			fprintf(f, " %s\n", reason);
 		else
@@ -2709,6 +2733,7 @@ static void Ban_Load_File(boolean warning)
 	FILE *f;
 	size_t i;
 	const char *address, *mask, *reason;
+	time_t unbanTime = NO_BAN_TIME;
 	char buffer[MAX_WADPATH];
 
 	f = fopen(va("%s"PATHSEP"%s", srb2home, "ban.txt"), "r");
@@ -2726,9 +2751,12 @@ static void Ban_Load_File(boolean warning)
 	{
 		address = strtok(buffer, " \t\r\n");
 		mask = strtok(NULL, " \t\r\n");
+		unbanTime = atoi(strtok(NULL, " \t\r\n"));
 		reason = strtok(NULL, "\r\n");
 
 		I_SetBanAddress(address, mask);
+		if (I_SetUnbanTime)
+			I_SetUnbanTime(unbanTime);
 		if (I_SetBanReason)
 			I_SetBanReason(reason);
 	}
@@ -3229,6 +3257,7 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 	XBOXSTATIC char buf[3 + MAX_REASONLENGTH];
 	char *reason = buf;
 	kickreason_t kickreason = KR_KICK;
+	UINT32 banMinutes = 0;
 
 	pnum = READUINT8(*p);
 	msg = READUINT8(*p);
@@ -3292,17 +3321,35 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 	// Save bans here. Used to be split between here and the actual command, depending on
 	// whenever the server did it or a remote admin did it, but it's simply more convenient
 	// to keep it all in one place.
-	if (server && (msg == KICK_MSG_BANNED || msg == KICK_MSG_CUSTOM_BAN))
+	if (server)
 	{
-		if (I_Ban && !I_Ban(playernode[(INT32)pnum]))
+		if (msg == KICK_MSG_GO_AWAY || msg == KICK_MSG_CUSTOM_KICK)
 		{
-			CONS_Alert(CONS_WARNING, M_GetText("Ban failed. Invalid node?\n"));
+			// Kick as a temporary ban.
+			banMinutes = cv_kicktime.value;
 		}
-		else
+
+		if (msg == KICK_MSG_BANNED || msg == KICK_MSG_CUSTOM_BAN || banMinutes)
 		{
-			if (I_SetBanReason)
-				I_SetBanReason(reason);
-			D_SaveBan();
+			if (I_Ban && !I_Ban(playernode[(INT32)pnum]))
+			{
+				CONS_Alert(CONS_WARNING, M_GetText("Ban failed. Invalid node?\n"));
+			}
+			else
+			{
+				if (I_SetBanReason)
+					I_SetBanReason(reason);
+
+				if (I_SetUnbanTime)
+				{
+					if (banMinutes)
+						I_SetUnbanTime(time(NULL) + (banMinutes * 60));
+					else
+						I_SetUnbanTime(NO_BAN_TIME);
+				}
+
+				D_SaveBan();
+			}
 		}
 	}
 
@@ -3388,9 +3435,13 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 #ifdef DUMPCONSISTENCY
 		if (msg == KICK_MSG_CON_FAIL) SV_SavedGame();
 #endif
+
+		LUAh_GameQuit(false);
+
 		D_QuitNetGame();
 		CL_Reset();
 		D_StartTitle();
+
 		if (msg == KICK_MSG_CON_FAIL)
 			M_StartMessage(M_GetText("Server closed connection\n(Synch failure)\nPress ESC\n"), NULL, MM_NOTHING);
 		else if (msg == KICK_MSG_PING_HIGH)
@@ -3566,6 +3617,7 @@ void D_ClientServerInit(void)
 #ifndef NONET
 	COM_AddCommand("getplayernum", Command_GetPlayerNum);
 	COM_AddCommand("kick", Command_Kick);
+	CV_RegisterVar(&cv_kicktime);
 	COM_AddCommand("ban", Command_Ban);
 	COM_AddCommand("banip", Command_BanIP);
 	COM_AddCommand("clearbans", Command_ClearBans);
@@ -4066,26 +4118,65 @@ static void HandleConnect(SINT8 node)
 	UINT8 maxplayers = min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxplayers.value);
 
 	if (bannednode && bannednode[node])
-		SV_SendRefuse(node, M_GetText("You have been banned\nfrom the server"));
+	{
+		if (bannednodetimeleft && bannednodetimeleft[node] != NO_BAN_TIME)
+		{
+			int minutes = bannednodetimeleft[node] / 60;
+			int hours = minutes / 60;
+
+			if (hours)
+			{
+				SV_SendRefuse(node, va(M_GetText("You have been temporarily\nkicked from the server.\n(Time remaining: %d hour%s)"), hours, hours > 1 ? "s" : ""));
+			}
+			else if (minutes)
+			{
+				SV_SendRefuse(node, va(M_GetText("You have been temporarily\nkicked from the server.\n(Time remaining: %d minute%s)"), minutes, minutes > 1 ? "s" : ""));
+			}
+			else
+			{
+				SV_SendRefuse(node, M_GetText("You have been temporarily\nkicked from the server.\n(Time remaining: <1 minute)"));
+			}
+		}
+		else
+		{
+			SV_SendRefuse(node, M_GetText("You have been banned\nfrom the server."));
+		}
+	}
 	else if (netbuffer->u.clientcfg._255 != 255 ||
 			netbuffer->u.clientcfg.packetversion != PACKETVERSION)
+	{
 		SV_SendRefuse(node, "Incompatible packet formats.");
+	}
 	else if (strncmp(netbuffer->u.clientcfg.application, SRB2APPLICATION,
 				sizeof netbuffer->u.clientcfg.application))
-		SV_SendRefuse(node, "Different SRB2 modifications\nare not compatible.");
+	{
+		SV_SendRefuse(node, "Different SRB2Kart modifications\nare not compatible.");
+	}
 	else if (netbuffer->u.clientcfg.version != VERSION
 		|| netbuffer->u.clientcfg.subversion != SUBVERSION)
+	{
 		SV_SendRefuse(node, va(M_GetText("Different SRB2Kart versions cannot\nplay a netgame!\n(server version %d.%d)"), VERSION, SUBVERSION));
+	}
 	else if (!cv_allownewplayer.value && node)
-		SV_SendRefuse(node, M_GetText("The server is not accepting\njoins for the moment"));
+	{
+		SV_SendRefuse(node, M_GetText("The server is not accepting\njoins for the moment."));
+	}
 	else if (D_NumPlayers() >= maxplayers)
+	{
 		SV_SendRefuse(node, va(M_GetText("Maximum players reached: %d"), maxplayers));
-	else if (netgame && D_NumPlayers() + netbuffer->u.clientcfg.localplayers > maxplayers)
-		SV_SendRefuse(node, va(M_GetText("Number of local players\nwould exceed maximum: %d"), maxplayers));
+	}
 	else if (netgame && netbuffer->u.clientcfg.localplayers > 4) // Hacked client?
+	{
 		SV_SendRefuse(node, M_GetText("Too many players from\nthis node."));
+	}
+	else if (netgame && D_NumPlayers() + netbuffer->u.clientcfg.localplayers > maxplayers)
+	{
+		SV_SendRefuse(node, va(M_GetText("Number of local players\nwould exceed maximum: %d"), maxplayers));
+	}
 	else if (netgame && !netbuffer->u.clientcfg.localplayers) // Stealth join?
+	{
 		SV_SendRefuse(node, M_GetText("No players from\nthis node."));
+	}
 	else
 	{
 #ifndef NONET
@@ -4319,13 +4410,13 @@ static void HandlePacketFromAwayNode(SINT8 node)
 					break;
 				}
 
-				M_StartMessage(va(M_GetText("Server refuses connection\n\nReason:\n%s"),
-					reason), NULL, MM_NOTHING);
-
 				D_QuitNetGame();
 				CL_Reset();
 				D_StartTitle();
 
+				M_StartMessage(va(M_GetText("Server refuses connection\n\nReason:\n%s"),
+					reason), NULL, MM_NOTHING);
+
 				free(reason);
 
 				// Will be reset by caller. Signals refusal.
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index 0ce20f1a3..c51af62b6 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -504,6 +504,8 @@ extern INT32 mapchangepending;
 extern doomdata_t *netbuffer;
 extern consvar_t cv_stunserver;
 extern consvar_t cv_httpsource;
+extern consvar_t cv_kicktime;
+
 extern consvar_t cv_showjoinaddress;
 extern consvar_t cv_playbackspeed;
 
diff --git a/src/d_net.c b/src/d_net.c
index b28aab7a0..70086ef2b 100644
--- a/src/d_net.c
+++ b/src/d_net.c
@@ -80,9 +80,12 @@ const char *(*I_GetNodeAddress) (INT32 node) = NULL;
 const char *(*I_GetBanAddress) (size_t ban) = NULL;
 const char *(*I_GetBanMask) (size_t ban) = NULL;
 const char *(*I_GetBanReason) (size_t ban) = NULL;
+time_t (*I_GetUnbanTime) (size_t ban) = NULL;
 boolean (*I_SetBanAddress) (const char *address, const char *mask) = NULL;
 boolean (*I_SetBanReason) (const char *reason) = NULL;
+boolean (*I_SetUnbanTime) (time_t timestamp) = NULL;
 boolean *bannednode = NULL;
+time_t *bannednodetimeleft = NULL;
 
 
 // network stats
diff --git a/src/i_net.h b/src/i_net.h
index 3608d81bc..9a2e11f4d 100644
--- a/src/i_net.h
+++ b/src/i_net.h
@@ -31,6 +31,8 @@
 ///  For use on the internet
 #define INETPACKETLENGTH 1024
 
+#define NO_BAN_TIME (time_t)(-1)
+
 extern INT16 hardware_MAXPACKETLENGTH;
 extern INT32 net_bandwidth; // in byte/s
 
@@ -146,9 +148,12 @@ extern const char *(*I_GetNodeAddress) (INT32 node);
 extern const char *(*I_GetBanAddress) (size_t ban);
 extern const char *(*I_GetBanMask) (size_t ban);
 extern const char *(*I_GetBanReason) (size_t ban);
+extern time_t (*I_GetUnbanTime) (size_t ban);
 extern boolean (*I_SetBanAddress) (const char *address,const char *mask);
 extern boolean (*I_SetBanReason) (const char *reason);
+extern boolean (*I_SetUnbanTime) (time_t timestamp);
 extern boolean *bannednode;
+extern time_t *bannednodetimeleft;
 
 /// \brief Called by D_SRB2Main to be defined by extern network driver
 boolean I_InitNetwork(void);
diff --git a/src/i_tcp.c b/src/i_tcp.c
index 75e3aaa6d..f425bbce6 100644
--- a/src/i_tcp.c
+++ b/src/i_tcp.c
@@ -236,7 +236,7 @@ typedef struct
 	mysockaddr_t address;
 	UINT8 mask;
 	char *reason;
-	// TODO: timestamp, for tempbans!
+	time_t timestamp;
 } banned_t;
 
 static SOCKET_TYPE mysockets[MAXNETNODES+1] = {ERRSOCKET};
@@ -254,6 +254,7 @@ static size_t numbans = 0;
 static size_t banned_size = 0;
 
 static boolean SOCK_bannednode[MAXNETNODES+1]; /// \note do we really need the +1?
+static time_t SOCK_bannednodetimeleft[MAXNETNODES+1];
 static boolean init_tcp_driver = false;
 
 static const char *serverport_name = DEFAULTPORT;
@@ -505,10 +506,24 @@ static const char *SOCK_GetBanReason(size_t ban)
 	(void)ban;
 	return NULL;
 #else
+	if (ban >= numbans)
+		return NULL;
 	return banned[ban].reason;
 #endif
 }
 
+static time_t SOCK_GetUnbanTime(size_t ban)
+{
+#ifdef NONET
+	(void)ban;
+	return NO_BAN_TIME;
+#else
+	if (ban >= numbans)
+		return NO_BAN_TIME;
+	return banned[ban].timestamp;
+#endif
+}
+
 #ifndef NONET
 static boolean SOCK_cmpaddr(mysockaddr_t *a, mysockaddr_t *b, UINT8 mask)
 {
@@ -656,6 +671,8 @@ static boolean SOCK_Get(void)
 			j = getfreenode();
 			if (j > 0)
 			{
+				const time_t curTime = time(NULL);
+
 				M_Memcpy(&clientaddress[j], &fromaddress, fromlen);
 				nodesocket[j] = mysockets[n];
 				DEBFILE(va("New node detected: node:%d address:%s\n", j,
@@ -668,13 +685,37 @@ static boolean SOCK_Get(void)
 				{
 					if (SOCK_cmpaddr(&fromaddress, &banned[i].address, banned[i].mask))
 					{
-						SOCK_bannednode[j] = true;
-						DEBFILE("This dude has been banned\n");
-						break;
+						if (banned[i].timestamp != NO_BAN_TIME)
+						{
+							if (curTime >= banned[i].timestamp)
+							{
+								SOCK_bannednodetimeleft[j] = NO_BAN_TIME;
+								SOCK_bannednode[j] = false;
+								DEBFILE("This dude was banned, but enough time has passed\n");
+								break;
+							}
+
+							SOCK_bannednodetimeleft[j] = banned[i].timestamp - curTime;
+							SOCK_bannednode[j] = true;
+							DEBFILE("This dude has been temporarily banned\n");
+							break;
+						}
+						else
+						{
+							SOCK_bannednodetimeleft[j] = NO_BAN_TIME;
+							SOCK_bannednode[j] = true;
+							DEBFILE("This dude has been banned\n");
+							break;
+						}
 					}
 				}
+
 				if (i == numbans)
+				{
+					SOCK_bannednodetimeleft[j] = NO_BAN_TIME;
 					SOCK_bannednode[j] = false;
+				}
+
 				return true;
 			}
 			else
@@ -1538,7 +1579,6 @@ static boolean SOCK_SetBanReason(const char *reason)
 	(void)reason;
 	return false;
 #else
-
 	if (!reason)
 	{
 		reason = "No reason given";
@@ -1549,6 +1589,17 @@ static boolean SOCK_SetBanReason(const char *reason)
 #endif
 }
 
+static boolean SOCK_SetUnbanTime(time_t timestamp)
+{
+#ifdef NONET
+	(void)reason;
+	return false;
+#else
+	banned[numbans - 1].timestamp = timestamp;
+	return true;
+#endif
+}
+
 static void SOCK_ClearBans(void)
 {
 	numbans = 0;
@@ -1646,9 +1697,12 @@ boolean I_InitTcpNetwork(void)
 	I_GetBanAddress = SOCK_GetBanAddress;
 	I_GetBanMask = SOCK_GetBanMask;
 	I_GetBanReason = SOCK_GetBanReason;
+	I_GetUnbanTime = SOCK_GetUnbanTime;
 	I_SetBanAddress = SOCK_SetBanAddress;
 	I_SetBanReason = SOCK_SetBanReason;
+	I_SetUnbanTime = SOCK_SetUnbanTime;
 	bannednode = SOCK_bannednode;
+	bannednodetimeleft = SOCK_bannednodetimeleft;
 
 	return ret;
 }
-- 
GitLab