diff --git a/GameServer/conf/world.xml b/GameServer/conf/world.xml index 4e7ed86..3b568c7 100644 --- a/GameServer/conf/world.xml +++ b/GameServer/conf/world.xml @@ -81,4 +81,12 @@ org.moparscape.msc.gs.persistence.impl.XMLUsingXStream + + + 15 + + true + + + 5 diff --git a/GameServer/src/org/moparscape/msc/config/Config.java b/GameServer/src/org/moparscape/msc/config/Config.java index 066c94e..3e30c12 100644 --- a/GameServer/src/org/moparscape/msc/config/Config.java +++ b/GameServer/src/org/moparscape/msc/config/Config.java @@ -43,6 +43,9 @@ public class Config { OS_LEVEL_THROTTLE_ALERT, OS_LEVEL_UNBLOCK_FAILED_ALERT, CONGRATS_FOR_MAX_LEVEL; public static String DATA_STORE; + public static int PACKET_PER_SECOND_THRESHOLD; + public static boolean PACKET_PER_SECOND_ALERT; + public static int AFK_TIMEOUT; static { loadEnv(); @@ -130,6 +133,11 @@ public class Config { .getProperty("max-level-congrats")); DATA_STORE = props.getProperty("data-store"); + + PACKET_PER_SECOND_THRESHOLD = Integer.parseInt(props.getProperty("packet-per-second-threshold")); + PACKET_PER_SECOND_ALERT = Boolean.parseBoolean(props.getProperty("packet-per-second-alert")); + + AFK_TIMEOUT = Integer.parseInt(props.getProperty("afk-timeout")); props.clear(); diff --git a/GameServer/src/org/moparscape/msc/gs/Server.java b/GameServer/src/org/moparscape/msc/gs/Server.java index f78b934..4ae7bab 100644 --- a/GameServer/src/org/moparscape/msc/gs/Server.java +++ b/GameServer/src/org/moparscape/msc/gs/Server.java @@ -14,6 +14,7 @@ import org.apache.mina.transport.socket.nio.SocketSessionConfig; import org.moparscape.msc.config.Config; import org.moparscape.msc.gs.connection.RSCConnectionHandler; import org.moparscape.msc.gs.connection.filter.ConnectionFilter; +import org.moparscape.msc.gs.connection.filter.PacketThrottler; import org.moparscape.msc.gs.core.GameEngine; import org.moparscape.msc.gs.core.LoginConnector; import org.moparscape.msc.gs.event.DelayedEvent; @@ -46,7 +47,6 @@ public class Server { displayConfigDefaulting(configFile); } - Config.initConfig(configFile); world = Instance.getWorld(); try { @@ -156,6 +156,8 @@ public class Server { acceptor = new SocketAcceptor(Runtime.getRuntime() .availableProcessors() + 1, Executors.newCachedThreadPool()); + acceptor.getFilterChain().addFirst("packetthrottler", + PacketThrottler.getInstance()); acceptor.getFilterChain().addFirst("connectionfilter", new ConnectionFilter()); IoAcceptorConfig config = new SocketAcceptorConfig(); @@ -252,7 +254,7 @@ public class Server { public static Server getServer() { return server; } - + private static void displayConfigDefaulting(String file) { System.out.println("Defaulting to use " + file); } diff --git a/GameServer/src/org/moparscape/msc/gs/connection/filter/PacketThrottler.java b/GameServer/src/org/moparscape/msc/gs/connection/filter/PacketThrottler.java new file mode 100644 index 0000000..c2dad4e --- /dev/null +++ b/GameServer/src/org/moparscape/msc/gs/connection/filter/PacketThrottler.java @@ -0,0 +1,113 @@ +package org.moparscape.msc.gs.connection.filter; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import net.jcip.annotations.ThreadSafe; + +import org.apache.mina.common.IoFilterAdapter; +import org.apache.mina.common.IoSession; +import org.moparscape.msc.config.Config; +import org.moparscape.msc.gs.Instance; +import org.moparscape.msc.gs.alert.AlertHandler; +import org.moparscape.msc.gs.event.DelayedEvent; +import org.moparscape.msc.gs.model.Player; +import org.moparscape.msc.gs.util.annotation.Singleton; + +/** + * + * This filter will disconnect players that have exceeded the packet per second + * threshold, and if configured, send an alert. + * + * @author CodeForFame + * + */ +@ThreadSafe +@Singleton +public class PacketThrottler extends IoFilterAdapter { + + ///////////////////////////////////////// + ///////// Singleton Boilerplate ///////// + ///////////////////////////////////////// + + private static final PacketThrottler instance = new PacketThrottler(); + + public static PacketThrottler getInstance() { + return instance; + } + + ///////////////////////////////////////// + /////// Singleton Boilerplate End /////// + ///////////////////////////////////////// + + /** + * The map of username hashes to packet per second. + */ + private Map playerToPacketCount = new ConcurrentHashMap(); + + private PacketThrottler() { + // Clears the count every second, so we can check the packets per second. + Instance.getDelayedEventHandler().add(new DelayedEvent(null, 1000) { + + @Override + public void run() { + playerToPacketCount.clear(); + } + + }); + } + + @Override + public void messageReceived(NextFilter nextFilter, IoSession session, + Object message) { + + Player player = (Player) session.getAttachment(); + if (session.isClosing() || player.destroyed()) { + return; + } + + int count = incrementAndGet(player.getUsernameHash()); + + if (count > Config.PACKET_PER_SECOND_THRESHOLD) { + + if (Config.PACKET_PER_SECOND_ALERT) { + // If the player is initialized, then use the username, + // otherwise use the IP. + String s = (player.isInitialized() ? player.getUsername() + : player.getCurrentIP()) + + " has exceeded the packet per second threshold"; + // Sends an alert with a priority of 2. + AlertHandler.sendAlert(s, 2); + } + + // Destroys the user and discards the packet. + player.destroy(true); + return; + } + + nextFilter.messageReceived(session, message); + } + + /** + * Increments and returns the current count. + * + * @param hash + * - The hash of the player. + */ + private int incrementAndGet(long hash) { + final int count; + + // Even though operations are atomic, there are multiple, therefore it + // needs to be synchronized. + synchronized (playerToPacketCount) { + + // If it is null, it will default to 0 + count = playerToPacketCount.get(hash) + 1; + + // Update/Create entry + playerToPacketCount.put(hash, count); + } + + return count; + } +} diff --git a/GameServer/src/org/moparscape/msc/gs/core/ClientUpdater.java b/GameServer/src/org/moparscape/msc/gs/core/ClientUpdater.java index 70a01c1..c3ce3b9 100644 --- a/GameServer/src/org/moparscape/msc/gs/core/ClientUpdater.java +++ b/GameServer/src/org/moparscape/msc/gs/core/ClientUpdater.java @@ -3,6 +3,7 @@ package org.moparscape.msc.gs.core; import java.util.ArrayList; import java.util.List; +import org.moparscape.msc.config.Config; import org.moparscape.msc.gs.Instance; import org.moparscape.msc.gs.builders.GameObjectPositionPacketBuilder; import org.moparscape.msc.gs.builders.ItemPositionPacketBuilder; @@ -396,13 +397,17 @@ public final class ClientUpdater { if (curTime - p.getLastPing() >= 30000) { p.destroy(false); } else if (p.warnedToMove()) { - if (curTime - p.getLastMoved() >= 960000 && p.loggedIn()) { + if (curTime - p.getLastMoved() >= ((Config.AFK_TIMEOUT + 1) * 60000) + && p.loggedIn()) { p.destroy(false); } - } else if (curTime - p.getLastMoved() >= 900000) { + } else if (curTime - p.getLastMoved() >= (Config.AFK_TIMEOUT * 60000) + && !p.warnedToMove()) { p.getActionSender() .sendMessage( - "@cya@You have not moved for 15 mins, please move to a new area to avoid logout."); + "@cya@You have not moved for " + + Config.AFK_TIMEOUT + + " mins, please move to a new area to avoid logout."); p.warnToMove(); } } diff --git a/GameServer/src/org/moparscape/msc/gs/util/annotation/Singleton.java b/GameServer/src/org/moparscape/msc/gs/util/annotation/Singleton.java new file mode 100644 index 0000000..5ee4599 --- /dev/null +++ b/GameServer/src/org/moparscape/msc/gs/util/annotation/Singleton.java @@ -0,0 +1,19 @@ +package org.moparscape.msc.gs.util.annotation; + +import java.lang.annotation.*; + +/** + * + * This annotation is used to denote that a Java class is a singleton. Scala + * objects do not need this annotation, because they are singletons by + * definition. + * + * @author CodeForFame + * + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface Singleton { + +}