Chat: new settings to prevent spam 4571/head
authorLoic Blot <loic.blot@unix-experience.fr>
Tue, 4 Oct 2016 16:17:12 +0000 (18:17 +0200)
committerLoic Blot <loic.blot@unix-experience.fr>
Tue, 4 Oct 2016 22:13:37 +0000 (00:13 +0200)
Added the following chat coreside features
* Chat messages length limit
* Message rate limiting
* Message rate kicking

Note:
* handleChat now takes RemotePlayer pointer instead of u16 to remove useless
  lookups

builtin/settingtypes.txt
minetest.conf.example
src/defaultsettings.cpp
src/network/serverpackethandler.cpp
src/player.cpp
src/player.h
src/server.cpp
src/server.h

index 15fab0f30e1d60e89f4127c0663b1b7c68301acb..95cc826a0a2295227b7063aebb13dc5dc54c550a 100644 (file)
@@ -774,6 +774,15 @@ time_speed (Time speed) int 72
 #    Interval of saving important changes in the world, stated in seconds.
 server_map_save_interval (Map save interval) float 5.3
 
+#    Set the maximum character length of a chat message sent by clients.
+# chat_message_max_size int 500
+
+#    Limit a single player to send X messages per 10 seconds.
+# chat_message_limit_per_10sec float 10.0
+
+#    Kick player if send more than X messages per 10 seconds.
+# chat_message_limit_trigger_kick int 50
+
 [**Physics]
 
 movement_acceleration_default (Default acceleration) float 3
index 9c80156257604aa090ba1cad2ee7dd007639eab2..b1b2027865ea8dea245dc3e434960043fb20361d 100644 (file)
 #    type: float
 # server_map_save_interval = 5.3
 
+#    Set the maximum character length of a chat message sent by clients. (0 to disable)
+#    type: integer
+# chat_message_max_size = 500
+
+#    Limit a single player to send X messages per 10 seconds. (0 to disable)
+#    type: float
+# chat_message_limit_per_10sec = 8.0
+
+#    Kick player if send more than X messages per 10 seconds. (0 to disable)
+#    type: integer
+# chat_message_limit_trigger_kick = 50
+
 ### Physics
 
 #    type: float
 # profiler.default_report_format = txt
 
 #    The file path relative to your worldpath in which profiles will be saved to.
-#    
+#
 #    type: string
 # profiler.report_path = ""
 
index 4520bac2f5cbaaa2b3ab72c6b1a4973307e5f4dd..00c233a426d9c8d21f01b2729a23914566480346 100644 (file)
@@ -285,6 +285,9 @@ void set_default_settings(Settings *settings)
        settings->setDefault("server_unload_unused_data_timeout", "29");
        settings->setDefault("max_objects_per_block", "49");
        settings->setDefault("server_map_save_interval", "5.3");
+       settings->setDefault("chat_message_max_size", "500");
+       settings->setDefault("chat_message_limit_per_10sec", "8.0");
+       settings->setDefault("chat_message_limit_trigger_kick", "50");
        settings->setDefault("sqlite_synchronous", "2");
        settings->setDefault("full_block_send_enable_min_time_from_building", "2.0");
        settings->setDefault("dedicated_server_step", "0.1");
index 1bcb78a8a109eeb89fefd1804392527d22432708..a8bfd9068841414a6475c8aed1b5365d1796d92e 100644 (file)
@@ -1065,7 +1065,7 @@ void Server::handleCommand_ChatMessage(NetworkPacket* pkt)
        std::wstring wname = narrow_to_wide(name);
 
        std::wstring answer_to_sender = handleChat(name, wname, message,
-               true, pkt->getPeerId());
+               true, dynamic_cast<RemotePlayer *>(player));
        if (!answer_to_sender.empty()) {
                // Send the answer to sender
                SendChatMessage(pkt->getPeerId(), answer_to_sender);
@@ -1656,16 +1656,16 @@ void Server::handleCommand_Interact(NetworkPacket* pkt)
                }
 
        } // action == 4
-       
+
        /*
                5: rightclick air
        */
        else if (action == 5) {
                ItemStack item = playersao->getWieldedItem();
-               
-               actionstream << player->getName() << " activates " 
+
+               actionstream << player->getName() << " activates "
                                << item.name << std::endl;
-               
+
                if (m_script->item_OnSecondaryUse(
                                item, playersao)) {
                        if( playersao->setWieldedItem(item)) {
index 5949712a50c8673c01ad33dc666185e76bb30487..fd72d63b6aaa7e3976665a4d309432c06923fccd 100644 (file)
@@ -227,10 +227,25 @@ void Player::clearHud()
        }
 }
 
+// static config cache for remoteplayer
+bool RemotePlayer::m_setting_cache_loaded = false;
+float RemotePlayer::m_setting_chat_message_limit_per_10sec = 0.0f;
+u16 RemotePlayer::m_setting_chat_message_limit_trigger_kick = 0;
+
 RemotePlayer::RemotePlayer(IGameDef *gamedef, const char *name):
        Player(gamedef, name),
-       m_sao(NULL)
+       m_sao(NULL),
+       m_last_chat_message_sent(time(NULL)),
+       m_chat_message_allowance(5.0f),
+       m_message_rate_overhead(0)
 {
+       if (!RemotePlayer::m_setting_cache_loaded) {
+               RemotePlayer::m_setting_chat_message_limit_per_10sec =
+                               g_settings->getFloat("chat_message_limit_per_10sec");
+               RemotePlayer::m_setting_chat_message_limit_trigger_kick =
+                               g_settings->getU16("chat_message_limit_trigger_kick");
+               RemotePlayer::m_setting_cache_loaded = true;
+       }
        movement_acceleration_default   = g_settings->getFloat("movement_acceleration_default")   * BS;
        movement_acceleration_air       = g_settings->getFloat("movement_acceleration_air")       * BS;
        movement_acceleration_fast      = g_settings->getFloat("movement_acceleration_fast")      * BS;
@@ -304,3 +319,42 @@ void RemotePlayer::setPosition(const v3f &position)
                m_sao->setBasePosition(position);
 }
 
+const RemotePlayerChatResult RemotePlayer::canSendChatMessage()
+{
+       // Rate limit messages
+       u32 now = time(NULL);
+       float time_passed = now - m_last_chat_message_sent;
+       m_last_chat_message_sent = now;
+
+       // If this feature is disabled
+       if (m_setting_chat_message_limit_per_10sec <= 0.0) {
+               return RPLAYER_CHATRESULT_OK;
+       }
+
+       m_chat_message_allowance += time_passed * (m_setting_chat_message_limit_per_10sec / 8.0f);
+       if (m_chat_message_allowance > m_setting_chat_message_limit_per_10sec) {
+               m_chat_message_allowance = m_setting_chat_message_limit_per_10sec;
+       }
+
+       if (m_chat_message_allowance < 1.0f) {
+               infostream << "Player " << m_name
+                               << " chat limited due to excessive message amount." << std::endl;
+
+               // Kick player if flooding is too intensive
+               m_message_rate_overhead++;
+               if (m_message_rate_overhead > RemotePlayer::m_setting_chat_message_limit_trigger_kick) {
+                       return RPLAYER_CHATRESULT_KICK;
+               }
+
+               return RPLAYER_CHATRESULT_FLOODING;
+       }
+
+       // Reinit message overhead
+       if (m_message_rate_overhead > 0) {
+               m_message_rate_overhead = 0;
+       }
+
+       m_chat_message_allowance -= 1.0f;
+       return RPLAYER_CHATRESULT_OK;
+}
+
index eab00bb0491768380aa21f9327b02e78829ae52c..f38effa9d62e20808ac0dd2b3a14665f3050b6ef 100644 (file)
@@ -439,7 +439,11 @@ private:
        Mutex m_mutex;
 };
 
-
+enum RemotePlayerChatResult {
+       RPLAYER_CHATRESULT_OK,
+       RPLAYER_CHATRESULT_FLOODING,
+       RPLAYER_CHATRESULT_KICK,
+};
 /*
        Player on the server
 */
@@ -457,8 +461,18 @@ public:
        { m_sao = sao; }
        void setPosition(const v3f &position);
 
+       const RemotePlayerChatResult canSendChatMessage();
+
 private:
        PlayerSAO *m_sao;
+
+       static bool m_setting_cache_loaded;
+       static float m_setting_chat_message_limit_per_10sec;
+       static u16 m_setting_chat_message_limit_trigger_kick;
+
+       u32 m_last_chat_message_sent;
+       float m_chat_message_allowance;
+       u16 m_message_rate_overhead;
 };
 
 #endif
index fb241f1799dfffc3b7ee909f2800a6e963c6bb59..c615aee137f9828dd35b5d558aa0d155f8778153 100644 (file)
@@ -358,6 +358,7 @@ Server::Server(
        add_legacy_abms(m_env, m_nodedef);
 
        m_liquid_transform_every = g_settings->getFloat("liquid_update");
+       m_max_chatmessage_length = g_settings->getU16("chat_message_max_size");
 }
 
 Server::~Server()
@@ -2734,8 +2735,7 @@ void Server::handleChatInterfaceEvent(ChatEvent *evt)
 }
 
 std::wstring Server::handleChat(const std::string &name, const std::wstring &wname,
-       const std::wstring &wmessage, bool check_shout_priv,
-       u16 peer_id_to_avoid_sending)
+       const std::wstring &wmessage, bool check_shout_priv, RemotePlayer *player)
 {
        // If something goes wrong, this player is to blame
        RollbackScopeActor rollback_scope(m_rollback,
@@ -2753,6 +2753,26 @@ std::wstring Server::handleChat(const std::string &name, const std::wstring &wna
        if (ate)
                return L"";
 
+       switch (player->canSendChatMessage()) {
+               case RPLAYER_CHATRESULT_FLOODING: {
+                       std::wstringstream ws;
+                       ws << L"You cannot send more messages. You are limited to "
+                          << g_settings->getFloat("chat_message_limit_per_10sec")
+                          << " messages per 10 seconds.";
+                       return ws.str();
+               }
+               case RPLAYER_CHATRESULT_KICK:
+                       DenyAccess_Legacy(player->peer_id, L"You have been kicked due to message flooding.");
+                       return L"";
+               case RPLAYER_CHATRESULT_OK: break;
+               default: FATAL_ERROR("Unhandled chat filtering result found.");
+       }
+
+       if (m_max_chatmessage_length > 0 && wmessage.length() > m_max_chatmessage_length) {
+               return L"Your message exceed the maximum chat message limit set on the server. "
+                       "It was refused. Send a shorter message";
+       }
+
        // Commands are implemented in Lua, so only catch invalid
        // commands that were not "eaten" and send an error back
        if (wmessage[0] == L'/') {
@@ -2787,6 +2807,7 @@ std::wstring Server::handleChat(const std::string &name, const std::wstring &wna
 
                std::vector<u16> clients = m_clients.getClientIDs();
 
+               u16 peer_id_to_avoid_sending = (player ? player->peer_id : PEER_ID_INEXISTENT);
                for (u16 i = 0; i < clients.size(); i++) {
                        u16 cid = clients[i];
                        if (cid != peer_id_to_avoid_sending)
index 7ee15a4638a5f1e75975e9ae61cf5e115eed6e5e..3ad894b389bc9d40274cbf015d711d154246815b 100644 (file)
@@ -487,7 +487,7 @@ private:
        std::wstring handleChat(const std::string &name, const std::wstring &wname,
                const std::wstring &wmessage,
                bool check_shout_priv = false,
-               u16 peer_id_to_avoid_sending = PEER_ID_INEXISTENT);
+               RemotePlayer *player = NULL);
        void handleAdminChat(const ChatEventChat *evt);
 
        v3f findSpawnPos();
@@ -522,6 +522,7 @@ private:
        // If true, do not allow multiple players and hide some multiplayer
        // functionality
        bool m_simple_singleplayer_mode;
+       u16 m_max_chatmessage_length;
 
        // Thread can set; step() will throw as ServerError
        MutexedVariable<std::string> m_async_fatal_error;