Feature #7756: Allow server to supply a reason to kicked/banned clients

This commit adds the missing feature of allowing the server owner to
    provide a reason for kicking/banning a client, which the client sees in
    a pop-up window after being kicked. The implementation extends the
    network protocol by adding a new network action called
    NETWORK_ACTION_KICKED that is capable of having an error string, unlike
    the other network error packages.  Additionally, the kick function
    broadcasts a message to all clients about the kicked client and the
    reason for the kick.
This commit is contained in:
Bjarni Thor 2020-01-21 15:39:10 +00:00 committed by Charles Pigott
parent b5d56559d2
commit 5880f1479f
9 changed files with 66 additions and 26 deletions

View File

@ -470,7 +470,7 @@ DEF_CONSOLE_CMD(ConClearBuffer)
* Network Core Console Commands * Network Core Console Commands
**********************************/ **********************************/
static bool ConKickOrBan(const char *argv, bool ban) static bool ConKickOrBan(const char *argv, bool ban, const char *reason)
{ {
uint n; uint n;
@ -494,14 +494,14 @@ static bool ConKickOrBan(const char *argv, bool ban)
if (!ban) { if (!ban) {
/* Kick only this client, not all clients with that IP */ /* Kick only this client, not all clients with that IP */
NetworkServerKickClient(client_id); NetworkServerKickClient(client_id, reason);
return true; return true;
} }
/* When banning, kick+ban all clients with that IP */ /* When banning, kick+ban all clients with that IP */
n = NetworkServerKickOrBanIP(client_id, ban); n = NetworkServerKickOrBanIP(client_id, ban, reason);
} else { } else {
n = NetworkServerKickOrBanIP(argv, ban); n = NetworkServerKickOrBanIP(argv, ban, reason);
} }
if (n == 0) { if (n == 0) {
@ -516,28 +516,48 @@ static bool ConKickOrBan(const char *argv, bool ban)
DEF_CONSOLE_CMD(ConKick) DEF_CONSOLE_CMD(ConKick)
{ {
if (argc == 0) { if (argc == 0) {
IConsoleHelp("Kick a client from a network game. Usage: 'kick <ip | client-id>'"); IConsoleHelp("Kick a client from a network game. Usage: 'kick <ip | client-id> [<kick-reason>]'");
IConsoleHelp("For client-id's, see the command 'clients'"); IConsoleHelp("For client-id's, see the command 'clients'");
return true; return true;
} }
if (argc != 2) return false; if (argc != 2 && argc != 3) return false;
return ConKickOrBan(argv[1], false); /* No reason supplied for kicking */
if (argc == 2) return ConKickOrBan(argv[1], false, nullptr);
/* Reason for kicking supplied */
int kick_message_length = strlen(argv[2]);
if (kick_message_length >= 255) {
IConsolePrintF(CC_ERROR, "ERROR: Maximum kick message length is 254 characters. You entered %d characters.", kick_message_length);
return false;
} else {
return ConKickOrBan(argv[1], false, argv[2]);
}
} }
DEF_CONSOLE_CMD(ConBan) DEF_CONSOLE_CMD(ConBan)
{ {
if (argc == 0) { if (argc == 0) {
IConsoleHelp("Ban a client from a network game. Usage: 'ban <ip | client-id>'"); IConsoleHelp("Ban a client from a network game. Usage: 'ban <ip | client-id> [<ban-reason>]'");
IConsoleHelp("For client-id's, see the command 'clients'"); IConsoleHelp("For client-id's, see the command 'clients'");
IConsoleHelp("If the client is no longer online, you can still ban his/her IP"); IConsoleHelp("If the client is no longer online, you can still ban his/her IP");
return true; return true;
} }
if (argc != 2) return false; if (argc != 2 && argc != 3) return false;
return ConKickOrBan(argv[1], true); /* No reason supplied for kicking */
if (argc == 2) return ConKickOrBan(argv[1], true, nullptr);
/* Reason for kicking supplied */
int kick_message_length = strlen(argv[2]);
if (kick_message_length >= 255) {
IConsolePrintF(CC_ERROR, "ERROR: Maximum kick message length is 254 characters. You entered %d characters.", kick_message_length);
return false;
} else {
return ConKickOrBan(argv[1], true, argv[2]);
}
} }
DEF_CONSOLE_CMD(ConUnBan) DEF_CONSOLE_CMD(ConUnBan)

View File

@ -2168,6 +2168,7 @@ STR_NETWORK_ERROR_WRONG_PASSWORD :{WHITE}Wrong pa
STR_NETWORK_ERROR_SERVER_FULL :{WHITE}The server is full STR_NETWORK_ERROR_SERVER_FULL :{WHITE}The server is full
STR_NETWORK_ERROR_SERVER_BANNED :{WHITE}You are banned from this server STR_NETWORK_ERROR_SERVER_BANNED :{WHITE}You are banned from this server
STR_NETWORK_ERROR_KICKED :{WHITE}You were kicked out of the game STR_NETWORK_ERROR_KICKED :{WHITE}You were kicked out of the game
STR_NETWORK_ERROR_KICK_MESSAGE :{WHITE}Reason: {RAW_STRING}
STR_NETWORK_ERROR_CHEATER :{WHITE}Cheating is not allowed on this server STR_NETWORK_ERROR_CHEATER :{WHITE}Cheating is not allowed on this server
STR_NETWORK_ERROR_TOO_MANY_COMMANDS :{WHITE}You were sending too many commands to the server STR_NETWORK_ERROR_TOO_MANY_COMMANDS :{WHITE}You were sending too many commands to the server
STR_NETWORK_ERROR_TIMEOUT_PASSWORD :{WHITE}You took too long to enter the password STR_NETWORK_ERROR_TIMEOUT_PASSWORD :{WHITE}You took too long to enter the password
@ -2227,6 +2228,7 @@ STR_NETWORK_MESSAGE_GIVE_MONEY :*** {RAW_STRING
STR_NETWORK_MESSAGE_GAVE_MONEY_AWAY :*** You gave {1:RAW_STRING} {2:CURRENCY_LONG} STR_NETWORK_MESSAGE_GAVE_MONEY_AWAY :*** You gave {1:RAW_STRING} {2:CURRENCY_LONG}
STR_NETWORK_MESSAGE_SERVER_SHUTDOWN :{WHITE}The server closed the session STR_NETWORK_MESSAGE_SERVER_SHUTDOWN :{WHITE}The server closed the session
STR_NETWORK_MESSAGE_SERVER_REBOOT :{WHITE}The server is restarting...{}Please wait... STR_NETWORK_MESSAGE_SERVER_REBOOT :{WHITE}The server is restarting...{}Please wait...
STR_NETWORK_MESSAGE_KICKED :*** {RAW_STRING} was kicked. Reason: ({RAW_STRING})
# Content downloading window # Content downloading window
STR_CONTENT_TITLE :{WHITE}Content downloading STR_CONTENT_TITLE :{WHITE}Content downloading

View File

@ -248,6 +248,7 @@ void NetworkTextMessage(NetworkAction action, TextColour colour, bool self_send,
case NETWORK_ACTION_GIVE_MONEY: strid = self_send ? STR_NETWORK_MESSAGE_GAVE_MONEY_AWAY : STR_NETWORK_MESSAGE_GIVE_MONEY; break; case NETWORK_ACTION_GIVE_MONEY: strid = self_send ? STR_NETWORK_MESSAGE_GAVE_MONEY_AWAY : STR_NETWORK_MESSAGE_GIVE_MONEY; break;
case NETWORK_ACTION_CHAT_COMPANY: strid = self_send ? STR_NETWORK_CHAT_TO_COMPANY : STR_NETWORK_CHAT_COMPANY; break; case NETWORK_ACTION_CHAT_COMPANY: strid = self_send ? STR_NETWORK_CHAT_TO_COMPANY : STR_NETWORK_CHAT_COMPANY; break;
case NETWORK_ACTION_CHAT_CLIENT: strid = self_send ? STR_NETWORK_CHAT_TO_CLIENT : STR_NETWORK_CHAT_CLIENT; break; case NETWORK_ACTION_CHAT_CLIENT: strid = self_send ? STR_NETWORK_CHAT_TO_CLIENT : STR_NETWORK_CHAT_CLIENT; break;
case NETWORK_ACTION_KICKED: strid = STR_NETWORK_MESSAGE_KICKED; break;
default: strid = STR_NETWORK_CHAT_ALL; break; default: strid = STR_NETWORK_CHAT_ALL; break;
} }

View File

@ -687,8 +687,15 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_ERROR(Packet *p
StringID err = STR_NETWORK_ERROR_LOSTCONNECTION; StringID err = STR_NETWORK_ERROR_LOSTCONNECTION;
if (error < (ptrdiff_t)lengthof(network_error_strings)) err = network_error_strings[error]; if (error < (ptrdiff_t)lengthof(network_error_strings)) err = network_error_strings[error];
/* In case of kicking a client, we assume there is a kick message in the packet if we can read one byte */
ShowErrorMessage(err, INVALID_STRING_ID, WL_CRITICAL); if (error == NETWORK_ERROR_KICKED && p->CanReadFromPacket(1)) {
char kick_msg[255];
p->Recv_string(kick_msg, sizeof(kick_msg));
SetDParamStr(0, kick_msg);
ShowErrorMessage(err, STR_NETWORK_ERROR_KICK_MESSAGE, WL_CRITICAL);
} else {
ShowErrorMessage(err, INVALID_STRING_ID, WL_CRITICAL);
}
/* Perform an emergency save if we had already entered the game */ /* Perform an emergency save if we had already entered the game */
if (this->status == STATUS_ACTIVE) ClientNetworkEmergencySave(); if (this->status == STATUS_ACTIVE) ClientNetworkEmergencySave();

View File

@ -75,9 +75,9 @@ void NetworkServerDoMove(ClientID client_id, CompanyID company_id);
void NetworkServerSendRcon(ClientID client_id, TextColour colour_code, const char *string); void NetworkServerSendRcon(ClientID client_id, TextColour colour_code, const char *string);
void NetworkServerSendChat(NetworkAction action, DestType type, int dest, const char *msg, ClientID from_id, int64 data = 0, bool from_admin = false); void NetworkServerSendChat(NetworkAction action, DestType type, int dest, const char *msg, ClientID from_id, int64 data = 0, bool from_admin = false);
void NetworkServerKickClient(ClientID client_id); void NetworkServerKickClient(ClientID client_id, const char *reason);
uint NetworkServerKickOrBanIP(ClientID client_id, bool ban); uint NetworkServerKickOrBanIP(ClientID client_id, bool ban, const char *reason);
uint NetworkServerKickOrBanIP(const char *ip, bool ban); uint NetworkServerKickOrBanIP(const char *ip, bool ban, const char *reason);
void NetworkInitChatMessage(); void NetworkInitChatMessage();
void CDECL NetworkAddChatMessage(TextColour colour, uint duration, const char *message, ...) WARN_FORMAT(3, 4); void CDECL NetworkAddChatMessage(TextColour colour, uint duration, const char *message, ...) WARN_FORMAT(3, 4);

View File

@ -1687,12 +1687,12 @@ static WindowDesc _client_list_popup_desc(
/* Here we start to define the options out of the menu */ /* Here we start to define the options out of the menu */
static void ClientList_Kick(const NetworkClientInfo *ci) static void ClientList_Kick(const NetworkClientInfo *ci)
{ {
NetworkServerKickClient(ci->client_id); NetworkServerKickClient(ci->client_id, nullptr);
} }
static void ClientList_Ban(const NetworkClientInfo *ci) static void ClientList_Ban(const NetworkClientInfo *ci)
{ {
NetworkServerKickOrBanIP(ci->client_id, true); NetworkServerKickOrBanIP(ci->client_id, true, nullptr);
} }
static void ClientList_GiveMoney(const NetworkClientInfo *ci) static void ClientList_GiveMoney(const NetworkClientInfo *ci)

View File

@ -407,13 +407,15 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::SendCompanyInfo()
/** /**
* Send an error to the client, and close its connection. * Send an error to the client, and close its connection.
* @param error The error to disconnect for. * @param error The error to disconnect for.
* @param reason In case of kicking a client, specifies the reason for kicking the client.
*/ */
NetworkRecvStatus ServerNetworkGameSocketHandler::SendError(NetworkErrorCode error) NetworkRecvStatus ServerNetworkGameSocketHandler::SendError(NetworkErrorCode error, const char *reason)
{ {
char str[100]; char str[100];
Packet *p = new Packet(PACKET_SERVER_ERROR); Packet *p = new Packet(PACKET_SERVER_ERROR);
p->Send_uint8(error); p->Send_uint8(error);
if (reason != nullptr) p->Send_string(reason);
this->SendPacket(p); this->SendPacket(p);
StringID strid = GetNetworkErrorMsg(error); StringID strid = GetNetworkErrorMsg(error);
@ -427,7 +429,11 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::SendError(NetworkErrorCode err
DEBUG(net, 1, "'%s' made an error and has been disconnected. Reason: '%s'", client_name, str); DEBUG(net, 1, "'%s' made an error and has been disconnected. Reason: '%s'", client_name, str);
NetworkTextMessage(NETWORK_ACTION_LEAVE, CC_DEFAULT, false, client_name, nullptr, strid); if (error == NETWORK_ERROR_KICKED && reason != nullptr) {
NetworkTextMessage(NETWORK_ACTION_KICKED, CC_DEFAULT, false, client_name, reason, strid);
} else {
NetworkTextMessage(NETWORK_ACTION_LEAVE, CC_DEFAULT, false, client_name, nullptr, strid);
}
for (NetworkClientSocket *new_cs : NetworkClientSocket::Iterate()) { for (NetworkClientSocket *new_cs : NetworkClientSocket::Iterate()) {
if (new_cs->status > STATUS_AUTHORIZED && new_cs != this) { if (new_cs->status > STATUS_AUTHORIZED && new_cs != this) {
@ -2039,29 +2045,32 @@ void NetworkServerSendRcon(ClientID client_id, TextColour colour_code, const cha
/** /**
* Kick a single client. * Kick a single client.
* @param client_id The client to kick. * @param client_id The client to kick.
* @param reason In case of kicking a client, specifies the reason for kicking the client.
*/ */
void NetworkServerKickClient(ClientID client_id) void NetworkServerKickClient(ClientID client_id, const char *reason)
{ {
if (client_id == CLIENT_ID_SERVER) return; if (client_id == CLIENT_ID_SERVER) return;
NetworkClientSocket::GetByClientID(client_id)->SendError(NETWORK_ERROR_KICKED); NetworkClientSocket::GetByClientID(client_id)->SendError(NETWORK_ERROR_KICKED, reason);
} }
/** /**
* Ban, or kick, everyone joined from the given client's IP. * Ban, or kick, everyone joined from the given client's IP.
* @param client_id The client to check for. * @param client_id The client to check for.
* @param ban Whether to ban or kick. * @param ban Whether to ban or kick.
* @param reason In case of kicking a client, specifies the reason for kicking the client.
*/ */
uint NetworkServerKickOrBanIP(ClientID client_id, bool ban) uint NetworkServerKickOrBanIP(ClientID client_id, bool ban, const char *reason)
{ {
return NetworkServerKickOrBanIP(NetworkClientSocket::GetByClientID(client_id)->GetClientIP(), ban); return NetworkServerKickOrBanIP(NetworkClientSocket::GetByClientID(client_id)->GetClientIP(), ban, reason);
} }
/** /**
* Kick or ban someone based on an IP address. * Kick or ban someone based on an IP address.
* @param ip The IP address/range to ban/kick. * @param ip The IP address/range to ban/kick.
* @param ban Whether to ban or just kick. * @param ban Whether to ban or just kick.
* @param reason In case of kicking a client, specifies the reason for kicking the client.
*/ */
uint NetworkServerKickOrBanIP(const char *ip, bool ban) uint NetworkServerKickOrBanIP(const char *ip, bool ban, const char *reason)
{ {
/* Add address to ban-list */ /* Add address to ban-list */
if (ban) { if (ban) {
@ -2081,7 +2090,7 @@ uint NetworkServerKickOrBanIP(const char *ip, bool ban)
for (NetworkClientSocket *cs : NetworkClientSocket::Iterate()) { for (NetworkClientSocket *cs : NetworkClientSocket::Iterate()) {
if (cs->client_id == CLIENT_ID_SERVER) continue; if (cs->client_id == CLIENT_ID_SERVER) continue;
if (cs->client_address.IsInNetmask(ip)) { if (cs->client_address.IsInNetmask(ip)) {
NetworkServerKickClient(cs->client_id); NetworkServerKickClient(cs->client_id, reason);
n++; n++;
} }
} }

View File

@ -89,7 +89,7 @@ public:
NetworkRecvStatus SendMove(ClientID client_id, CompanyID company_id); NetworkRecvStatus SendMove(ClientID client_id, CompanyID company_id);
NetworkRecvStatus SendClientInfo(NetworkClientInfo *ci); NetworkRecvStatus SendClientInfo(NetworkClientInfo *ci);
NetworkRecvStatus SendError(NetworkErrorCode error); NetworkRecvStatus SendError(NetworkErrorCode error, const char *reason = nullptr);
NetworkRecvStatus SendChat(NetworkAction action, ClientID client_id, bool self_send, const char *msg, int64 data); NetworkRecvStatus SendChat(NetworkAction action, ClientID client_id, bool self_send, const char *msg, int64 data);
NetworkRecvStatus SendJoin(ClientID client_id); NetworkRecvStatus SendJoin(ClientID client_id);
NetworkRecvStatus SendFrame(); NetworkRecvStatus SendFrame();

View File

@ -85,6 +85,7 @@ enum DestType {
enum NetworkAction { enum NetworkAction {
NETWORK_ACTION_JOIN, NETWORK_ACTION_JOIN,
NETWORK_ACTION_LEAVE, NETWORK_ACTION_LEAVE,
NETWORK_ACTION_KICKED,
NETWORK_ACTION_SERVER_MESSAGE, NETWORK_ACTION_SERVER_MESSAGE,
NETWORK_ACTION_CHAT, NETWORK_ACTION_CHAT,
NETWORK_ACTION_CHAT_COMPANY, NETWORK_ACTION_CHAT_COMPANY,