Add a ChatFilter class and use it.
Refactored out the flood protection, empty message, and message too long errors out to the new class. This will also easily allow adding other chat protections in the future, in one location. Further refactoring should be possible to not have to hold chat-protection related information in the User object, as well.
This commit is contained in:
parent
4e2b5e480b
commit
c6f62c1496
|
@ -10,6 +10,10 @@ pyx.insecure_id_allowed=true
|
|||
pyx.id_code_salt=
|
||||
# comma-separated listed of IP addresses (v4 or v6) from which users are considered admins.
|
||||
pyx.admin_addrs=127.0.0.1,::1
|
||||
# this many messages in that many seconds is considered chatting too fast.
|
||||
pyx.flood_count=4
|
||||
# seconds
|
||||
pyx.flood_time=30
|
||||
|
||||
# for production use, use postgres
|
||||
#hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
||||
|
|
|
@ -6,6 +6,8 @@ pyx.server.broadcast_connects_and_disconnects=${pyx.broadcast_connects_and_disco
|
|||
pyx.server.global_chat_enabled=${pyx.global_chat_enabled}
|
||||
pyx.server.id_code_salt=${pyx.id_code_salt}
|
||||
pyx.server.admin_addrs=${pyx.admin_addrs}
|
||||
pyx.chat.flood_count=${pyx.flood_count}
|
||||
pyx.chat.flood_time=${pyx.flood_time}
|
||||
pyx.build=${buildNumber}
|
||||
|
||||
# this is NOT allowed to be changed during a reload, as metrics depend on previous events
|
||||
|
|
|
@ -28,10 +28,11 @@ import java.util.Map;
|
|||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import net.socialgamer.cah.CahModule.GlobalChatEnabled;
|
||||
import net.socialgamer.cah.Constants;
|
||||
import net.socialgamer.cah.Constants.AjaxOperation;
|
||||
import net.socialgamer.cah.Constants.AjaxRequest;
|
||||
import net.socialgamer.cah.Constants.ErrorCode;
|
||||
|
@ -43,6 +44,7 @@ import net.socialgamer.cah.RequestWrapper;
|
|||
import net.socialgamer.cah.data.ConnectedUsers;
|
||||
import net.socialgamer.cah.data.QueuedMessage.MessageType;
|
||||
import net.socialgamer.cah.data.User;
|
||||
import net.socialgamer.cah.util.ChatFilter;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -52,16 +54,19 @@ import net.socialgamer.cah.data.User;
|
|||
*/
|
||||
public class ChatHandler extends Handler {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ChatHandler.class);
|
||||
public static final String OP = AjaxOperation.CHAT.toString();
|
||||
|
||||
private final ChatFilter chatFilter;
|
||||
private final ConnectedUsers users;
|
||||
private final boolean globalChatEnabled;
|
||||
|
||||
@Inject
|
||||
public ChatHandler(final ConnectedUsers users,
|
||||
@GlobalChatEnabled final boolean globalChatEnabled) {
|
||||
@GlobalChatEnabled final boolean globalChatEnabled, final ChatFilter chatFilter) {
|
||||
this.users = users;
|
||||
this.globalChatEnabled = globalChatEnabled;
|
||||
this.chatFilter = chatFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -86,39 +91,37 @@ public class ChatHandler extends Handler {
|
|||
} else {
|
||||
final String message = request.getParameter(AjaxRequest.MESSAGE).trim();
|
||||
|
||||
// Intentionally leaving flood protection as per-user, rather than
|
||||
// changing it to per-user-per-game.
|
||||
if (user.getLastMessageTimes().size() >= Constants.CHAT_FLOOD_MESSAGE_COUNT) {
|
||||
final Long head = user.getLastMessageTimes().get(0);
|
||||
if (System.currentTimeMillis() - head < Constants.CHAT_FLOOD_TIME) {
|
||||
final ChatFilter.Result filterResult = chatFilter.filterGlobal(user, message);
|
||||
switch (filterResult) {
|
||||
case OK:
|
||||
// nothing to do
|
||||
break;
|
||||
case TOO_FAST:
|
||||
return error(ErrorCode.TOO_FAST);
|
||||
}
|
||||
user.getLastMessageTimes().remove(0);
|
||||
case TOO_LONG:
|
||||
return error(ErrorCode.MESSAGE_TOO_LONG);
|
||||
case NO_MESSAGE:
|
||||
return error(ErrorCode.NO_MSG_SPECIFIED);
|
||||
default:
|
||||
LOG.error(String.format("Unknown chat filter result %s", filterResult));
|
||||
}
|
||||
|
||||
if (message.length() > Constants.CHAT_MAX_LENGTH) {
|
||||
return error(ErrorCode.MESSAGE_TOO_LONG);
|
||||
} else if (message.length() == 0) {
|
||||
return error(ErrorCode.NO_MSG_SPECIFIED);
|
||||
} else {
|
||||
user.getLastMessageTimes().add(System.currentTimeMillis());
|
||||
final HashMap<ReturnableData, Object> broadcastData = new HashMap<ReturnableData, Object>();
|
||||
broadcastData.put(LongPollResponse.EVENT, LongPollEvent.CHAT.toString());
|
||||
broadcastData.put(LongPollResponse.FROM, user.getNickname());
|
||||
broadcastData.put(LongPollResponse.MESSAGE, message);
|
||||
broadcastData.put(LongPollResponse.ID_CODE, user.getIdCode());
|
||||
broadcastData.put(LongPollResponse.SIGIL, user.getSigil().toString());
|
||||
if (user.isAdmin()) {
|
||||
broadcastData.put(LongPollResponse.FROM_ADMIN, true);
|
||||
}
|
||||
if (wall) {
|
||||
broadcastData.put(LongPollResponse.WALL, true);
|
||||
}
|
||||
if (emote) {
|
||||
broadcastData.put(LongPollResponse.EMOTE, true);
|
||||
}
|
||||
users.broadcastToAll(MessageType.CHAT, broadcastData);
|
||||
final HashMap<ReturnableData, Object> broadcastData = new HashMap<ReturnableData, Object>();
|
||||
broadcastData.put(LongPollResponse.EVENT, LongPollEvent.CHAT.toString());
|
||||
broadcastData.put(LongPollResponse.FROM, user.getNickname());
|
||||
broadcastData.put(LongPollResponse.MESSAGE, message);
|
||||
broadcastData.put(LongPollResponse.ID_CODE, user.getIdCode());
|
||||
broadcastData.put(LongPollResponse.SIGIL, user.getSigil().toString());
|
||||
if (user.isAdmin()) {
|
||||
broadcastData.put(LongPollResponse.FROM_ADMIN, true);
|
||||
}
|
||||
if (wall) {
|
||||
broadcastData.put(LongPollResponse.WALL, true);
|
||||
}
|
||||
if (emote) {
|
||||
broadcastData.put(LongPollResponse.EMOTE, true);
|
||||
}
|
||||
users.broadcastToAll(MessageType.CHAT, broadcastData);
|
||||
}
|
||||
|
||||
return data;
|
||||
|
|
|
@ -28,9 +28,10 @@ import java.util.Map;
|
|||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import net.socialgamer.cah.Constants;
|
||||
import net.socialgamer.cah.Constants.AjaxOperation;
|
||||
import net.socialgamer.cah.Constants.AjaxRequest;
|
||||
import net.socialgamer.cah.Constants.ErrorCode;
|
||||
|
@ -42,6 +43,7 @@ import net.socialgamer.cah.data.Game;
|
|||
import net.socialgamer.cah.data.GameManager;
|
||||
import net.socialgamer.cah.data.QueuedMessage.MessageType;
|
||||
import net.socialgamer.cah.data.User;
|
||||
import net.socialgamer.cah.util.ChatFilter;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -51,11 +53,15 @@ import net.socialgamer.cah.data.User;
|
|||
*/
|
||||
public class GameChatHandler extends GameWithPlayerHandler {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(GameChatHandler.class);
|
||||
public static final String OP = AjaxOperation.GAME_CHAT.toString();
|
||||
|
||||
private final ChatFilter chatFilter;
|
||||
|
||||
@Inject
|
||||
public GameChatHandler(final GameManager gameManager) {
|
||||
public GameChatHandler(final GameManager gameManager, final ChatFilter chatFilter) {
|
||||
super(gameManager);
|
||||
this.chatFilter = chatFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -70,33 +76,31 @@ public class GameChatHandler extends GameWithPlayerHandler {
|
|||
} else {
|
||||
final String message = request.getParameter(AjaxRequest.MESSAGE).trim();
|
||||
|
||||
// Intentionally leaving flood protection as per-user, rather than
|
||||
// changing it to per-user-per-game.
|
||||
if (user.getLastMessageTimes().size() >= Constants.CHAT_FLOOD_MESSAGE_COUNT) {
|
||||
final Long head = user.getLastMessageTimes().get(0);
|
||||
if (System.currentTimeMillis() - head < Constants.CHAT_FLOOD_TIME) {
|
||||
final ChatFilter.Result filterResult = chatFilter.filterGame(user, message);
|
||||
switch (filterResult) {
|
||||
case OK:
|
||||
// nothing to do
|
||||
break;
|
||||
case TOO_FAST:
|
||||
return error(ErrorCode.TOO_FAST);
|
||||
}
|
||||
user.getLastMessageTimes().remove(0);
|
||||
case TOO_LONG:
|
||||
return error(ErrorCode.MESSAGE_TOO_LONG);
|
||||
case NO_MESSAGE:
|
||||
return error(ErrorCode.NO_MSG_SPECIFIED);
|
||||
default:
|
||||
LOG.error(String.format("Unknown chat filter result %s", filterResult));
|
||||
}
|
||||
|
||||
if (message.length() > Constants.CHAT_MAX_LENGTH) {
|
||||
return error(ErrorCode.MESSAGE_TOO_LONG);
|
||||
} else if (message.length() == 0) {
|
||||
return error(ErrorCode.NO_MSG_SPECIFIED);
|
||||
} else {
|
||||
user.getLastMessageTimes().add(System.currentTimeMillis());
|
||||
final HashMap<ReturnableData, Object> broadcastData = new HashMap<ReturnableData, Object>();
|
||||
broadcastData.put(LongPollResponse.EVENT, LongPollEvent.CHAT.toString());
|
||||
broadcastData.put(LongPollResponse.FROM, user.getNickname());
|
||||
broadcastData.put(LongPollResponse.MESSAGE, message);
|
||||
broadcastData.put(LongPollResponse.FROM_ADMIN, user.isAdmin());
|
||||
broadcastData.put(LongPollResponse.ID_CODE, user.getIdCode());
|
||||
broadcastData.put(LongPollResponse.SIGIL, user.getSigil().toString());
|
||||
broadcastData.put(LongPollResponse.GAME_ID, game.getId());
|
||||
broadcastData.put(LongPollResponse.EMOTE, emote);
|
||||
game.broadcastToPlayers(MessageType.CHAT, broadcastData);
|
||||
}
|
||||
final HashMap<ReturnableData, Object> broadcastData = new HashMap<ReturnableData, Object>();
|
||||
broadcastData.put(LongPollResponse.EVENT, LongPollEvent.CHAT.toString());
|
||||
broadcastData.put(LongPollResponse.FROM, user.getNickname());
|
||||
broadcastData.put(LongPollResponse.MESSAGE, message);
|
||||
broadcastData.put(LongPollResponse.FROM_ADMIN, user.isAdmin());
|
||||
broadcastData.put(LongPollResponse.ID_CODE, user.getIdCode());
|
||||
broadcastData.put(LongPollResponse.SIGIL, user.getSigil().toString());
|
||||
broadcastData.put(LongPollResponse.GAME_ID, game.getId());
|
||||
broadcastData.put(LongPollResponse.EMOTE, emote);
|
||||
game.broadcastToPlayers(MessageType.CHAT, broadcastData);
|
||||
}
|
||||
|
||||
return data;
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* Copyright (c) 2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
* provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this list of conditions
|
||||
* and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright notice, this list of
|
||||
* conditions and the following disclaimer in the documentation and/or other materials provided
|
||||
* with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
|
||||
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
|
||||
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package net.socialgamer.cah.util;
|
||||
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import net.socialgamer.cah.Constants;
|
||||
import net.socialgamer.cah.data.User;
|
||||
|
||||
|
||||
/**
|
||||
* Filter for chat messages. Currently handles flood limiting, can be extended with a lot more.
|
||||
*/
|
||||
@Singleton
|
||||
public class ChatFilter {
|
||||
private static final Logger LOG = Logger.getLogger(ChatFilter.class);
|
||||
|
||||
private static final int DEFAULT_CHAT_FLOOD_MESSAGE_COUNT = 4;
|
||||
private static final long DEFAULT_CHAT_FLOOD_TIME = TimeUnit.SECONDS.toMillis(30);
|
||||
|
||||
private final Provider<Properties> propsProvider;
|
||||
|
||||
public enum Result {
|
||||
OK, TOO_FAST, TOO_LONG, NO_MESSAGE
|
||||
}
|
||||
|
||||
@Inject
|
||||
public ChatFilter(final Provider<Properties> propsProvider) {
|
||||
this.propsProvider = propsProvider;
|
||||
}
|
||||
|
||||
public Result filterGlobal(final User user, final String message) {
|
||||
final Result result = filterCommon(user, message);
|
||||
if (Result.OK != result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO
|
||||
user.getLastMessageTimes().add(System.currentTimeMillis());
|
||||
return result;
|
||||
}
|
||||
|
||||
public Result filterGame(final User user, final String message) {
|
||||
final Result result = filterCommon(user, message);
|
||||
if (Result.OK != result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO
|
||||
user.getLastMessageTimes().add(System.currentTimeMillis());
|
||||
return result;
|
||||
}
|
||||
|
||||
private Result filterCommon(final User user, final String message) {
|
||||
// TODO
|
||||
|
||||
// Intentionally leaving flood protection as per-user, rather than
|
||||
// changing it to per-user-per-game.
|
||||
if (user.getLastMessageTimes().size() >= getFloodCount()) {
|
||||
final Long head = user.getLastMessageTimes().get(0);
|
||||
if (System.currentTimeMillis() - head < getFloodTime()) {
|
||||
return Result.TOO_FAST;
|
||||
}
|
||||
user.getLastMessageTimes().remove(0);
|
||||
}
|
||||
|
||||
if (message.length() > Constants.CHAT_MAX_LENGTH) {
|
||||
return Result.TOO_LONG;
|
||||
} else if (message.length() == 0) {
|
||||
return Result.NO_MESSAGE;
|
||||
}
|
||||
|
||||
return Result.OK;
|
||||
}
|
||||
|
||||
private int getFloodCount() {
|
||||
try {
|
||||
return Integer.parseInt(propsProvider.get().getProperty("pyx.chat.flood_count",
|
||||
String.valueOf(DEFAULT_CHAT_FLOOD_MESSAGE_COUNT)));
|
||||
} catch (final NumberFormatException e) {
|
||||
LOG.warn(String.format("Unable to parse pyx.chat.flood_count as a number,"
|
||||
+ " using default of %d", DEFAULT_CHAT_FLOOD_MESSAGE_COUNT), e);
|
||||
return DEFAULT_CHAT_FLOOD_MESSAGE_COUNT;
|
||||
}
|
||||
}
|
||||
|
||||
private long getFloodTime() {
|
||||
try {
|
||||
return TimeUnit.SECONDS.toMillis(Integer.parseInt(propsProvider.get().getProperty(
|
||||
"pyx.chat.flood_time", String.valueOf(DEFAULT_CHAT_FLOOD_TIME))));
|
||||
} catch (final NumberFormatException e) {
|
||||
LOG.warn(String.format("Unable to parse pyx.chat.flood_time as a number,"
|
||||
+ " using default of %d", DEFAULT_CHAT_FLOOD_TIME), e);
|
||||
return DEFAULT_CHAT_FLOOD_TIME;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue