diff --git a/src/d_clisrv.c b/src/d_clisrv.c index 22bd659c..e31ccb56 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 = {"kicktime", "10", CV_SAVE, CV_Unsigned, NULL, 0, NULL, NULL, 0, 0, NULL}; + static inline void *G_DcpyTiccmd(void* dest, const ticcmd_t* src, const size_t n) { const size_t d = n / sizeof(ticcmd_t); @@ -2637,57 +2639,87 @@ static void CL_ConnectToServer(void) } #ifndef NONET -typedef struct banreason_s -{ - char *reason; - struct banreason_s *prev; //-1 - struct banreason_s *next; //+1 -} banreason_t; - -static banreason_t *reasontail = NULL; //last entry, use prev -static banreason_t *reasonhead = NULL; //1st entry, use next - static void Command_ShowBan(void) //Print out ban list { size_t i; - const char *address, *mask; - banreason_t *reasonlist = reasonhead; + const char *address, *mask, *reason, *username; + time_t unbanTime = NO_BAN_TIME; + const time_t curTime = time(NULL); if (I_GetBanAddress) CONS_Printf(M_GetText("Ban List:\n")); else return; - for (i = 0;(address = I_GetBanAddress(i)) != NULL;i++) + for (i = 0; (address = I_GetBanAddress(i)) != NULL; i++) { + unbanTime = NO_BAN_TIME; + if (I_GetUnbanTime) + unbanTime = I_GetUnbanTime(i); + + if (unbanTime != NO_BAN_TIME && curTime >= unbanTime) + continue; + + CONS_Printf("%s: ", sizeu1(i+1)); + + if (I_GetBanUsername && (username = I_GetBanUsername(i)) != NULL) + CONS_Printf("%s - ", username); + if (!I_GetBanMask || (mask = I_GetBanMask(i)) == NULL) - CONS_Printf("%s: %s ", sizeu1(i+1), address); + CONS_Printf("%s", address); else - CONS_Printf("%s: %s/%s ", sizeu1(i+1), address, mask); + CONS_Printf("%s/%s", address, mask); - if (reasonlist && reasonlist->reason) - CONS_Printf("(%s)\n", reasonlist->reason); - else - CONS_Printf("\n"); + if (I_GetBanReason && (reason = I_GetBanReason(i)) != NULL) + CONS_Printf(" - %s", reason); - if (reasonlist) reasonlist = reasonlist->next; + if (unbanTime != NO_BAN_TIME) + { + // these are fudged a little to match what a joiner sees + int minutes = ((unbanTime - curTime) + 30) / 60; + int hours = (minutes + 1) / 60; + int days = (hours + 1) / 24; + if (days) + CONS_Printf(" (%d day%s)", days, days > 1 ? "s" : ""); + else if (hours) + CONS_Printf(" (%d hour%s)", hours, hours > 1 ? "s" : ""); + else if (minutes) + CONS_Printf(" (%d minute%s)", minutes, minutes > 1 ? "s" : ""); + else + CONS_Printf(" (<1 minute)"); + } + + CONS_Printf("\n"); } if (i == 0 && !address) CONS_Printf(M_GetText("(empty)\n")); } +static boolean bansLoaded = false; +// If you're a community contributor looking to improve how bans are written, please +// offer your changes back to our Git repository. Kart Krew reserve the right to +// utilise format numbers in use by community builds for different layouts. +#define BANFORMAT 1 + void D_SaveBan(void) { FILE *f; size_t i; - banreason_t *reasonlist = reasonhead; const char *address, *mask; + const char *username, *reason; + const time_t curTime = time(NULL); + time_t unbanTime = NO_BAN_TIME; + const char *path = va("%s"PATHSEP"%s", srb2home, "ban.txt"); - if (!reasonhead) + if (bansLoaded != true) + { + // You didn't even get to ATTEMPT to load bans.txt. + // Don't immediately save nothing over it. return; + } - f = fopen(va("%s"PATHSEP"%s", srb2home, "ban.txt"), "w"); + f = fopen(path, "w"); if (!f) { @@ -2695,67 +2727,76 @@ void D_SaveBan(void) return; } - for (i = 0;(address = I_GetBanAddress(i)) != NULL;i++) + // Add header. + fprintf(f, "BANFORMAT %d\n", BANFORMAT); + + for (i = 0; (address = I_GetBanAddress(i)) != NULL; i++) { + if (I_GetUnbanTime) + { + unbanTime = I_GetUnbanTime(i); + } + else + { + unbanTime = NO_BAN_TIME; + } + + if (unbanTime != NO_BAN_TIME && curTime >= unbanTime) + { + // This one has served their sentence. + // We don't need to save them in the file anymore. + continue; + } + + mask = NULL; if (!I_GetBanMask || (mask = I_GetBanMask(i)) == NULL) - fprintf(f, "%s 0", address); + fprintf(f, "%s/0", address); else - fprintf(f, "%s %s", address, mask); + fprintf(f, "%s/%s", address, mask); - if (reasonlist && reasonlist->reason) - fprintf(f, " %s\n", reasonlist->reason); + fprintf(f, " %ld", (long)unbanTime); + + username = NULL; + if (I_GetBanUsername && (username = I_GetBanUsername(i)) != NULL) + fprintf(f, " \"%s\"", username); else - fprintf(f, " %s\n", "NA"); + fprintf(f, " \"%s\"", "Direct IP ban"); - if (reasonlist) reasonlist = reasonlist->next; + reason = NULL; + if (I_GetBanReason && (reason = I_GetBanReason(i)) != NULL) + fprintf(f, " \"%s\"\n", reason); + else + fprintf(f, " \"%s\"\n", "No reason given"); } fclose(f); } -static void Ban_Add(const char *reason) -{ - banreason_t *reasonlist = malloc(sizeof(*reasonlist)); - - if (!reasonlist) - return; - if (!reason) - reason = "NA"; - - reasonlist->next = NULL; - reasonlist->reason = Z_StrDup(reason); - if ((reasonlist->prev = reasontail) == NULL) - reasonhead = reasonlist; - else - reasontail->next = reasonlist; - reasontail = reasonlist; -} - static void Command_ClearBans(void) { - banreason_t *temp; - if (!I_ClearBans) return; I_ClearBans(); D_SaveBan(); - reasontail = NULL; - while (reasonhead) - { - temp = reasonhead->next; - Z_Free(reasonhead->reason); - free(reasonhead); - reasonhead = temp; - } } -static void Ban_Load_File(boolean warning) +void D_LoadBan(boolean warning) { FILE *f; - size_t i; - const char *address, *mask; + size_t i, j; + char *address, *mask; + char *username, *reason; + time_t unbanTime = NO_BAN_TIME; char buffer[MAX_WADPATH]; + UINT8 banmode = 0; + boolean malformed = false; + + if (!I_ClearBans) + return; + + // We at least attempted loading bans.txt + bansLoaded = true; f = fopen(va("%s"PATHSEP"%s", srb2home, "ban.txt"), "r"); @@ -2766,30 +2807,115 @@ static void Ban_Load_File(boolean warning) return; } - if (I_ClearBans) - Command_ClearBans(); - else - { - fclose(f); - return; - } + I_ClearBans(); - for (i=0; fgets(buffer, (int)sizeof(buffer), f); i++) + for (i = 0; fgets(buffer, (int)sizeof(buffer), f); i++) { - address = strtok(buffer, " \t\r\n"); + address = strtok(buffer, " /\t\r\n"); mask = strtok(NULL, " \t\r\n"); - I_SetBanAddress(address, mask); + if (i == 0 && !strncmp(address, "BANFORMAT", 9)) + { + if (mask) + { + banmode = atoi(mask); + } + switch (banmode) + { + case BANFORMAT: // currently supported format + //case 0: -- permitted only when BANFORMAT string not present + break; + default: + { + fclose(f); + CONS_Alert(CONS_WARNING, "Could not load unknown ban.txt for ban list (BANFORMAT %s, expected %d)\n", mask, BANFORMAT); + return; + } + } + continue; + } - Ban_Add(strtok(NULL, "\r\n")); + if (I_SetBanAddress(address, mask) == false) // invalid IP input? + { + CONS_Alert(CONS_WARNING, "\"%s/%s\" is not a valid IP address, discarding...\n", address, mask); + continue; + } + + // One-way legacy format conversion -- the game will crash otherwise + if (banmode == 0) + { + unbanTime = NO_BAN_TIME; + username = NULL; // not guaranteed to be accurate, but only sane substitute + reason = strtok(NULL, "\r\n"); + if (reason && reason[0] == 'N' && reason[1] == 'A' && reason[2] == '\0') + { + reason = NULL; + } + } + else + { + reason = strtok(NULL, " \"\t\r\n"); + if (reason) + { + unbanTime = atoi(reason); + reason = NULL; + } + else + { + unbanTime = NO_BAN_TIME; + malformed = true; + } + + username = strtok(NULL, "\"\t\r\n"); // go until next " + if (!username) + { + malformed = true; + } + + strtok(NULL, "\"\t\r\n"); // remove first " + reason = strtok(NULL, "\"\r\n"); // go until next " + if (!reason) + { + malformed = true; + } + } + + // Enforce MAX_REASONLENGTH. + if (reason) + { + j = 0; + while (reason[j] != '\0') + { + if ((j++) < MAX_REASONLENGTH) + continue; + reason[j] = '\0'; + break; + } + } + + if (I_SetUnbanTime) + I_SetUnbanTime(unbanTime); + + if (I_SetBanUsername) + I_SetBanUsername(username); + + if (I_SetBanReason) + I_SetBanReason(reason); + } + + if (malformed) + { + CONS_Alert(CONS_WARNING, "One or more lines of ban.txt are malformed. The game can correct for this, but some data may be lost.\n"); } fclose(f); } +#undef BANFORMAT + static void Command_ReloadBan(void) //recheck ban.txt { - Ban_Load_File(true); + D_LoadBan(true); } static void Command_connect(void) @@ -3125,7 +3251,7 @@ static void Command_Ban(void) { if (COM_Argc() < 2) { - CONS_Printf(M_GetText("Ban : ban and kick a player\n")); + CONS_Printf(M_GetText("ban : ban and kick a player\n")); return; } @@ -3140,83 +3266,95 @@ static void Command_Ban(void) XBOXSTATIC UINT8 buf[3 + MAX_REASONLENGTH]; UINT8 *p = buf; const SINT8 pn = nametonum(COM_Argv(1)); - const INT32 node = playernode[(INT32)pn]; if (pn == -1 || pn == 0) return; WRITEUINT8(p, pn); - if (server && I_Ban && !I_Ban(node)) // only the server is allowed to do this right now + if (COM_Argc() == 2) { - CONS_Alert(CONS_WARNING, M_GetText("Too many bans! Geez, that's a lot of people you're excluding...\n")); - WRITEUINT8(p, KICK_MSG_GO_AWAY); + WRITEUINT8(p, KICK_MSG_BANNED); SendNetXCmd(XD_KICK, &buf, 2); } else { - if (server) // only the server is allowed to do this right now + size_t i, j = COM_Argc(); + char message[MAX_REASONLENGTH]; + + //Steal from the motd code so you don't have to put the reason in quotes. + strlcpy(message, COM_Argv(2), sizeof message); + for (i = 3; i < j; i++) { - Ban_Add(COM_Argv(2)); - D_SaveBan(); // save the ban list + strlcat(message, " ", sizeof message); + strlcat(message, COM_Argv(i), sizeof message); } - if (COM_Argc() == 2) - { - WRITEUINT8(p, KICK_MSG_BANNED); - SendNetXCmd(XD_KICK, &buf, 2); - } - else - { - size_t i, j = COM_Argc(); - char message[MAX_REASONLENGTH]; - - //Steal from the motd code so you don't have to put the reason in quotes. - strlcpy(message, COM_Argv(2), sizeof message); - for (i = 3; i < j; i++) - { - strlcat(message, " ", sizeof message); - strlcat(message, COM_Argv(i), sizeof message); - } - - WRITEUINT8(p, KICK_MSG_CUSTOM_BAN); - WRITESTRINGN(p, message, MAX_REASONLENGTH); - SendNetXCmd(XD_KICK, &buf, p - buf); - } + WRITEUINT8(p, KICK_MSG_CUSTOM_BAN); + WRITESTRINGN(p, message, MAX_REASONLENGTH); + SendNetXCmd(XD_KICK, &buf, p - buf); } } else CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n")); - } static void Command_BanIP(void) { - if (COM_Argc() < 2) + size_t ac = COM_Argc(); + + if (ac < 2) { - CONS_Printf(M_GetText("banip : ban an ip address\n")); + CONS_Printf(M_GetText("banip []: ban an ip address\n")); return; } if (server) // Only the server can use this, otherwise does nothing. { - const char *address = (COM_Argv(1)); - const char *reason; + char *addressInput = Z_StrDup(COM_Argv(1)); - if (COM_Argc() == 2) - reason = NULL; - else + const char *address = NULL; + const char *mask = NULL; + + const char *reason = NULL; + + address = strtok(addressInput, "/"); + mask = strtok(NULL, ""); + + if (ac > 2) + { reason = COM_Argv(2); + } - - if (I_SetBanAddress && I_SetBanAddress(address, NULL)) + if (I_SetBanAddress && I_SetBanAddress(address, mask)) { if (reason) - CONS_Printf("Banned IP address %s for: %s\n", address, reason); + { + CONS_Printf( + "Banned IP address %s%s for: %s\n", + address, + (mask && (strlen(mask) > 0)) ? va("/%s", mask) : "", + reason + ); + } else - CONS_Printf("Banned IP address %s\n", address); + { + CONS_Printf( + "Banned IP address %s%s\n", + address, + (mask && (strlen(mask) > 0)) ? va("/%s", mask) : "" + ); + } + + if (I_SetUnbanTime) + I_SetUnbanTime(NO_BAN_TIME); + + if (I_SetBanUsername) + I_SetBanUsername(NULL); + + if (I_SetBanReason) + I_SetBanReason(reason); - Ban_Add(reason); D_SaveBan(); } else @@ -3297,6 +3435,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); @@ -3355,18 +3494,49 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum) msg = KICK_MSG_CON_FAIL; } + if (msg == KICK_MSG_CUSTOM_BAN || msg == KICK_MSG_CUSTOM_KICK) + { + READSTRINGN(*p, reason, MAX_REASONLENGTH+1); + } + //CONS_Printf("\x82%s ", player_names[pnum]); - // If a verified admin banned someone, the server needs to know about it. - // If the playernum isn't zero (the server) then the server needs to record the ban. - if (server && playernum && (msg == KICK_MSG_BANNED || msg == KICK_MSG_CUSTOM_BAN)) + // 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) { - if (I_Ban && !I_Ban(playernode[(INT32)pnum])) - CONS_Alert(CONS_WARNING, M_GetText("Too many bans! Geez, that's a lot of people you're excluding...\n")); -#ifndef NONET - else - Ban_Add(reason); -#endif + if (msg == KICK_MSG_GO_AWAY || msg == KICK_MSG_CUSTOM_KICK) + { + // Kick as a temporary ban. + banMinutes = cv_kicktime.value; + } + + if (msg == KICK_MSG_BANNED || msg == KICK_MSG_CUSTOM_BAN || banMinutes) + { + if (I_Ban && !I_Ban(playernode[(INT32)pnum])) + { + CONS_Alert(CONS_WARNING, M_GetText("Ban failed. Invalid node?\n")); + } + else + { + if (I_SetBanUsername) + I_SetBanUsername(player_names[pnum]); + + 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(); + } + } } if (msg == KICK_MSG_PLAYER_QUIT) @@ -3435,12 +3605,10 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum) kickreason = KR_BAN; break; case KICK_MSG_CUSTOM_KICK: - READSTRINGN(*p, reason, MAX_REASONLENGTH+1); HU_AddChatText(va("\x82*%s has been kicked (%s)", player_names[pnum], reason), false); kickreason = KR_KICK; break; case KICK_MSG_CUSTOM_BAN: - READSTRINGN(*p, reason, MAX_REASONLENGTH+1); HU_AddChatText(va("\x82*%s has been banned (%s)", player_names[pnum], reason), false); kickreason = KR_BAN; break; @@ -3454,6 +3622,7 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum) 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) @@ -3461,9 +3630,9 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum) else if (msg == KICK_MSG_BANNED) M_StartMessage(M_GetText("You have been banned by the server\n\nPress ESC\n"), NULL, MM_NOTHING); else if (msg == KICK_MSG_CUSTOM_KICK) - M_StartMessage(va(M_GetText("You have been kicked\n(%s)\nPress ESC\n"), reason), NULL, MM_NOTHING); + M_StartMessage(va(M_GetText("You have been kicked\n(%s)\n\nPress ESC\n"), reason), NULL, MM_NOTHING); else if (msg == KICK_MSG_CUSTOM_BAN) - M_StartMessage(va(M_GetText("You have been banned\n(%s)\nPress ESC\n"), reason), NULL, MM_NOTHING); + M_StartMessage(va(M_GetText("You have been banned\n(%s)\n\nPress ESC\n"), reason), NULL, MM_NOTHING); else M_StartMessage(M_GetText("You have been kicked by the server\n\nPress ESC\n"), NULL, MM_NOTHING); } @@ -3629,6 +3798,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); @@ -3656,7 +3826,7 @@ void D_ClientServerInit(void) #ifdef DUMPCONSISTENCY CV_RegisterVar(&cv_dumpconsistency); #endif - Ban_Load_File(false); + D_LoadBan(false); #endif gametic = 0; @@ -3681,6 +3851,8 @@ static void ResetNode(INT32 node) nodewaiting[node] = 0; playerpernode[node] = 0; sendingsavegame[node] = false; + bannednode[node].banid = SIZE_MAX; + bannednode[node].timeleft = NO_BAN_TIME; } void SV_ResetServer(void) @@ -4128,27 +4300,78 @@ static void HandleConnect(SINT8 node) // It's too much effort to legimately fix right now. Just prevent it from reaching that state. 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 (bannednode && bannednode[node].banid != SIZE_MAX) + { + const char *reason = NULL; + + // Get the reason... + if (!I_GetBanReason || (reason = I_GetBanReason(bannednode[node].banid)) == NULL) + reason = "No reason given"; + + if (bannednode[node].timeleft != NO_BAN_TIME) + { + // these are fudged a little to allow it to sink in for impatient rejoiners + int minutes = (bannednode[node].timeleft + 30) / 60; + int hours = (minutes + 1) / 60; + int days = (hours + 1) / 24; + + if (days) + { + SV_SendRefuse(node, va("K|%s\n(Time remaining: %d day%s)", reason, days, days > 1 ? "s" : "")); + } + else if (hours) + { + SV_SendRefuse(node, va("K|%s\n(Time remaining: %d hour%s)", reason, hours, hours > 1 ? "s" : "")); + } + else if (minutes) + { + SV_SendRefuse(node, va("K|%s\n(Time remaining: %d minute%s)", reason, minutes, minutes > 1 ? "s" : "")); + } + else + { + SV_SendRefuse(node, va("K|%s\n(Time remaining: <1 minute)", reason)); + } + } + else + { + SV_SendRefuse(node, va("B|%s", reason)); + } + } 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 @@ -4382,13 +4605,22 @@ 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(); + if (reason[1] == '|') + { + M_StartMessage(va("You have been %sfrom the server\n\nReason:\n%s", + (reason[0] == 'B') ? "banned\n" : "temporarily\nkicked ", + reason+2), NULL, MM_NOTHING); + } + else + { + 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 0ce20f1a..c51af62b 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 d974d6cf..cccd2088 100644 --- a/src/d_net.c +++ b/src/d_net.c @@ -79,8 +79,14 @@ void (*I_ClearBans)(void) = NULL; 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_GetBanUsername) (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 *bannednode = NULL; +boolean (*I_SetBanUsername) (const char *username) = NULL; +boolean (*I_SetBanReason) (const char *reason) = NULL; +boolean (*I_SetUnbanTime) (time_t timestamp) = NULL; +bannednode_t *bannednode = NULL; // network stats diff --git a/src/d_net.h b/src/d_net.h index 607f3e90..e267f526 100644 --- a/src/d_net.h +++ b/src/d_net.h @@ -55,6 +55,7 @@ boolean HGetPacket(void); void D_SetDoomcom(void); #ifndef NONET void D_SaveBan(void); +void D_LoadBan(boolean warning); #endif boolean D_CheckNetGame(void); void D_CloseConnection(void); diff --git a/src/d_netcmd.c b/src/d_netcmd.c index a192feda..9fa5db78 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -1035,7 +1035,16 @@ void D_RegisterClientCommands(void) * \sa CleanupPlayerName, SetPlayerName, Got_NameAndColor * \author Graue */ -static boolean IsNameGood(char *name, INT32 playernum) + +static boolean AllowedPlayerNameChar(char ch) +{ + if (!isprint(ch) || ch == ';' || ch == '"' || (UINT8)(ch) >= 0x80) + return false; + + return true; +} + +static boolean EnsurePlayerNameIsGood(char *name, INT32 playernum) { INT32 ix; @@ -1056,7 +1065,7 @@ static boolean IsNameGood(char *name, INT32 playernum) // Also, anything over 0x80 is disallowed too, since compilers love to // differ on whether they're printable characters or not. for (ix = 0; name[ix] != '\0'; ix++) - if (!isprint(name[ix]) || name[ix] == ';' || (UINT8)(name[ix]) >= 0x80) + if (!AllowedPlayerNameChar(name[ix])) return false; // Check if a player is currently using the name, case-insensitively. @@ -1076,14 +1085,14 @@ static boolean IsNameGood(char *name, INT32 playernum) if (len > 1) { name[len-1] = '\0'; - if (!IsNameGood (name, playernum)) + if (!EnsurePlayerNameIsGood(name, playernum)) return false; } else if (len == 1) // Agh! { // Last ditch effort... sprintf(name, "%d", M_RandomKey(10)); - if (!IsNameGood (name, playernum)) + if (!EnsurePlayerNameIsGood(name, playernum)) return false; } else @@ -1142,6 +1151,16 @@ static void CleanupPlayerName(INT32 playernum, const char *newname) tmpname = p; + do + { + if (!AllowedPlayerNameChar(*p)) + break; + } + while (*++p) ; + + if (*p)/* bad char found */ + break; + // Remove trailing spaces. p = &tmpname[strlen(tmpname)-1]; // last character while (*p == ' ' && p >= tmpname) @@ -1213,12 +1232,12 @@ static void CleanupPlayerName(INT32 playernum, const char *newname) * \param newname New name for that player. Should be good, but won't * necessarily be if the client is maliciously modified or * buggy. - * \sa CleanupPlayerName, IsNameGood + * \sa CleanupPlayerName, EnsurePlayerNameIsGood * \author Graue */ static void SetPlayerName(INT32 playernum, char *newname) { - if (IsNameGood(newname, playernum)) + if (EnsurePlayerNameIsGood(newname, playernum)) { if (strcasecmp(newname, player_names[playernum]) != 0) { diff --git a/src/i_net.h b/src/i_net.h index 0e17077b..a5b1aba8 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 @@ -145,8 +147,20 @@ extern void (*I_ClearBans)(void); 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_GetBanUsername) (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 *bannednode; +extern boolean (*I_SetBanUsername) (const char *username); +extern boolean (*I_SetBanReason) (const char *reason); +extern boolean (*I_SetUnbanTime) (time_t timestamp); + +typedef struct +{ + size_t banid; + time_t timeleft; +} bannednode_t; +extern bannednode_t *bannednode; /// \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 ba973494..36740eb8 100644 --- a/src/i_tcp.c +++ b/src/i_tcp.c @@ -174,8 +174,6 @@ static UINT8 UPNP_support = TRUE; #endif // !NONET -#define MAXBANS 100 - #include "i_system.h" #include "i_net.h" #include "d_net.h" @@ -183,6 +181,7 @@ static UINT8 UPNP_support = TRUE; #include "i_tcp.h" #include "m_argv.h" #include "stun.h" +#include "z_zone.h" #include "doomstat.h" @@ -231,6 +230,16 @@ typedef int socklen_t; #endif #ifndef NONET + +typedef struct +{ + mysockaddr_t address; + UINT8 mask; + char *username; + char *reason; + time_t timestamp; +} banned_t; + static SOCKET_TYPE mysockets[MAXNETNODES+1] = {ERRSOCKET}; static size_t mysocketses = 0; static int myfamily[MAXNETNODES+1] = {0}; @@ -239,12 +248,13 @@ static mysockaddr_t clientaddress[MAXNETNODES+1]; static mysockaddr_t broadcastaddress[MAXNETNODES+1]; static size_t broadcastaddresses = 0; static boolean nodeconnected[MAXNETNODES+1]; -static mysockaddr_t banned[MAXBANS]; -static UINT8 bannedmask[MAXBANS]; +static banned_t *banned; #endif static size_t numbans = 0; -static boolean SOCK_bannednode[MAXNETNODES+1]; /// \note do we really need the +1? +static size_t banned_size = 0; + +static bannednode_t SOCK_bannednode[MAXNETNODES+1]; /// \note do we really need the +1? static boolean init_tcp_driver = false; static const char *serverport_name = DEFAULTPORT; @@ -472,7 +482,7 @@ static const char *SOCK_GetBanAddress(size_t ban) #ifdef NONET return NULL; #else - return SOCK_AddrToStr(&banned[ban]); + return SOCK_AddrToStr(&banned[ban].address); #endif } @@ -484,12 +494,48 @@ static const char *SOCK_GetBanMask(size_t ban) static char s[16]; //255.255.255.255 netmask? no, just CDIR for only if (ban >= numbans) return NULL; - if (sprintf(s,"%d",bannedmask[ban]) > 0) + if (sprintf(s,"%d",banned[ban].mask) > 0) return s; #endif return NULL; } +static const char *SOCK_GetBanUsername(size_t ban) +{ +#ifdef NONET + (void)ban; + return NULL; +#else + if (ban >= numbans) + return NULL; + return banned[ban].username; +#endif +} + +static const char *SOCK_GetBanReason(size_t ban) +{ +#ifdef NONET + (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) { @@ -637,6 +683,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, @@ -647,15 +695,39 @@ static boolean SOCK_Get(void) // check if it's a banned dude so we can send a refusal later for (i = 0; i < numbans; i++) { - if (SOCK_cmpaddr(&fromaddress, &banned[i], bannedmask[i])) + 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_bannednode[j].timeleft = NO_BAN_TIME; + SOCK_bannednode[j].banid = SIZE_MAX; + DEBFILE("This dude was banned, but enough time has passed\n"); + break; + } + + SOCK_bannednode[j].timeleft = banned[i].timestamp - curTime; + SOCK_bannednode[j].banid = i; + DEBFILE("This dude has been temporarily banned\n"); + break; + } + else + { + SOCK_bannednode[j].timeleft = NO_BAN_TIME; + SOCK_bannednode[j].banid = i; + DEBFILE("This dude has been banned\n"); + break; + } } } + if (i == numbans) - SOCK_bannednode[j] = false; + { + SOCK_bannednode[j].timeleft = NO_BAN_TIME; + SOCK_bannednode[j].banid = SIZE_MAX; + } + return true; } else @@ -1395,30 +1467,116 @@ static boolean SOCK_OpenSocket(void) #endif } +static void AddBannedIndex(void) +{ + if (numbans >= banned_size) + { + if (banned_size == 0) + { + banned_size = 8; + } + else + { + banned_size *= 2; + } + + banned = Z_ReallocAlign( + (void*) banned, + sizeof(banned_t) * banned_size, + PU_STATIC, + NULL, + sizeof(banned_t) * 8 + ); + } + + numbans++; +} + static boolean SOCK_Ban(INT32 node) { + INT32 ban; + if (node > MAXNETNODES) return false; + #ifdef NONET + (void)ban; return false; #else - if (numbans == MAXBANS) - return false; - M_Memcpy(&banned[numbans], &clientaddress[node], sizeof (mysockaddr_t)); - if (banned[numbans].any.sa_family == AF_INET) + ban = numbans; + AddBannedIndex(); + + M_Memcpy(&banned[ban].address, &clientaddress[node], sizeof (mysockaddr_t)); + + if (banned[ban].address.any.sa_family == AF_INET) { - banned[numbans].ip4.sin_port = 0; - bannedmask[numbans] = 32; + banned[ban].address.ip4.sin_port = 0; + banned[ban].mask = 32; } #ifdef HAVE_IPV6 - else if (banned[numbans].any.sa_family == AF_INET6) + else if (banned[ban].address.any.sa_family == AF_INET6) { - banned[numbans].ip6.sin6_port = 0; - bannedmask[numbans] = 128; + banned[ban].address.ip6.sin6_port = 0; + banned[ban].mask = 128; } #endif - numbans++; + + return true; +#endif +} + +static boolean SOCK_SetBanUsername(const char *username) +{ +#ifdef NONET + (void)username; + return false; +#else + if (username == NULL || strlen(username) == 0) + { + username = "Direct IP ban"; + } + + if (banned[numbans - 1].username) + { + Z_Free(banned[numbans - 1].username); + banned[numbans - 1].username = NULL; + } + + banned[numbans - 1].username = Z_StrDup(username); + return true; +#endif +} + +static boolean SOCK_SetBanReason(const char *reason) +{ +#ifdef NONET + (void)reason; + return false; +#else + if (reason == NULL || strlen(reason) == 0) + { + reason = "No reason given"; + } + + if (banned[numbans - 1].reason) + { + Z_Free(banned[numbans - 1].reason); + banned[numbans - 1].reason = NULL; + } + + banned[numbans - 1].reason = Z_StrDup(reason); + return true; +#endif +} + +static boolean SOCK_SetUnbanTime(time_t timestamp) +{ +#ifdef NONET + (void)reason; + return false; +#else + banned[numbans - 1].timestamp = timestamp; return true; #endif } @@ -1433,7 +1591,7 @@ static boolean SOCK_SetBanAddress(const char *address, const char *mask) struct my_addrinfo *ai, *runp, hints; int gaie; - if (numbans == MAXBANS || !address) + if (!address) return false; memset(&hints, 0x00, sizeof(hints)); @@ -1448,26 +1606,42 @@ static boolean SOCK_SetBanAddress(const char *address, const char *mask) runp = ai; - while(runp != NULL && numbans != MAXBANS) + while (runp != NULL) { - memcpy(&banned[numbans], runp->ai_addr, runp->ai_addrlen); + INT32 ban; + UINT8 numericalmask; + + ban = numbans; + AddBannedIndex(); + + memcpy(&banned[ban].address, runp->ai_addr, runp->ai_addrlen); + +#ifdef HAVE_IPV6 + if (runp->ai_family == AF_INET6) + banned[ban].mask = 128; + else +#endif + banned[ban].mask = 32; if (mask) - bannedmask[numbans] = (UINT8)atoi(mask); -#ifdef HAVE_IPV6 - else if (runp->ai_family == AF_INET6) - bannedmask[numbans] = 128; -#endif + { + numericalmask = (UINT8)atoi(mask); + } else - bannedmask[numbans] = 32; + { + numericalmask = 0; + } + + if (numericalmask > 0 && numericalmask < banned[ban].mask) + { + banned[ban].mask = numericalmask; + } + + // Set defaults, in case anything funny happens. + SOCK_SetBanUsername(NULL); + SOCK_SetBanReason(NULL); + SOCK_SetUnbanTime(NO_BAN_TIME); - if (bannedmask[numbans] > 32 && runp->ai_family == AF_INET) - bannedmask[numbans] = 32; -#ifdef HAVE_IPV6 - else if (bannedmask[numbans] > 128 && runp->ai_family == AF_INET6) - bannedmask[numbans] = 128; -#endif - numbans++; runp = runp->ai_next; } @@ -1480,6 +1654,9 @@ static boolean SOCK_SetBanAddress(const char *address, const char *mask) static void SOCK_ClearBans(void) { numbans = 0; + banned_size = 0; + Z_Free(banned); + banned = NULL; } boolean I_InitTcpNetwork(void) @@ -1571,7 +1748,13 @@ boolean I_InitTcpNetwork(void) I_GetNodeAddress = SOCK_GetNodeAddress; I_GetBanAddress = SOCK_GetBanAddress; I_GetBanMask = SOCK_GetBanMask; + I_GetBanUsername = SOCK_GetBanUsername; + I_GetBanReason = SOCK_GetBanReason; + I_GetUnbanTime = SOCK_GetUnbanTime; I_SetBanAddress = SOCK_SetBanAddress; + I_SetBanUsername = SOCK_SetBanUsername; + I_SetBanReason = SOCK_SetBanReason; + I_SetUnbanTime = SOCK_SetUnbanTime; bannednode = SOCK_bannednode; return ret; diff --git a/src/m_menu.c b/src/m_menu.c index 09d0f393..b0738da3 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -4567,7 +4567,11 @@ void M_StartMessage(const char *string, void *routine, } if (i == strlen(message+start)) + { start += i; + if (i > max) + max = i; + } } MessageDef.x = (INT16)((BASEVIDWIDTH - 8*max-16)/2);