diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java index 3bc8c709..f772f483 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java @@ -70,9 +70,9 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin core, - ProxiedPlayer player, Server server) { - super(core, player, core.getPlugin().getSession().get(player.getPendingConnection())); + ProxiedPlayer player, Server server, BungeeLoginSession session) { + super(core, player, session); this.server = server; } diff --git a/core/src/main/java/com/github/games647/fastlogin/core/RateLimiter.java b/core/src/main/java/com/github/games647/fastlogin/core/RateLimiter.java new file mode 100644 index 00000000..63e64244 --- /dev/null +++ b/core/src/main/java/com/github/games647/fastlogin/core/RateLimiter.java @@ -0,0 +1,41 @@ +package com.github.games647.fastlogin.core; + +/** + * Limit the number of requests with a maximum size. Each requests expires after the specified time making it available + * for another request. + */ +public class RateLimiter { + + private final long[] requests; + private final long expireTime; + private int position; + + public RateLimiter(int maxLimit, long expireTime) { + this.requests = new long[maxLimit]; + this.expireTime = expireTime; + } + + /** + * Ask if access is allowed. If so register the request. + * + * @return true if allowed + */ + public boolean tryAcquire() { + // currentTime millis could be expensive on some systems + long now = System.currentTimeMillis(); + + // after this the request should be expired + long toBeExpired = now - expireTime; + synchronized (this) { + // having synchronized will limit the amount of concurrency a lot + long oldest = requests[position]; + if (oldest < toBeExpired) { + requests[position] = now; + position = (position + 1) % requests.length; + return true; + } + + return false; + } + } +} diff --git a/core/src/main/java/com/github/games647/fastlogin/core/shared/FastLoginCore.java b/core/src/main/java/com/github/games647/fastlogin/core/shared/FastLoginCore.java index af234f4c..9dd732ff 100644 --- a/core/src/main/java/com/github/games647/fastlogin/core/shared/FastLoginCore.java +++ b/core/src/main/java/com/github/games647/fastlogin/core/shared/FastLoginCore.java @@ -4,6 +4,7 @@ import com.github.games647.craftapi.resolver.MojangResolver; import com.github.games647.craftapi.resolver.http.RotatingProxySelector; import com.github.games647.fastlogin.core.AuthStorage; import com.github.games647.fastlogin.core.CommonUtil; +import com.github.games647.fastlogin.core.RateLimiter; import com.github.games647.fastlogin.core.hooks.AuthPlugin; import com.github.games647.fastlogin.core.hooks.DefaultPasswordGenerator; import com.github.games647.fastlogin.core.hooks.PasswordGenerator; @@ -55,6 +56,7 @@ public class FastLoginCore

> { private Configuration config; private AuthStorage storage; + private RateLimiter rateLimiter; private PasswordGenerator

passwordGenerator = new DefaultPasswordGenerator<>(); private AuthPlugin

authPlugin; @@ -82,8 +84,12 @@ public class FastLoginCore

> { }); } catch (IOException ioEx) { plugin.getLog().error("Failed to load yaml files", ioEx); + return; } + int maxCon = config.getInt("anti-bot.connections"); + int expireTime = config.getInt("anti-bot.expire"); + rateLimiter = new RateLimiter(maxCon, expireTime * 60 * 1_000); Set proxies = config.getStringList("proxies") .stream() .map(HostAndPort::fromString) @@ -217,6 +223,10 @@ public class FastLoginCore

> { return authPlugin; } + public RateLimiter getRateLimiter() { + return rateLimiter; + } + public void setAuthPluginHook(AuthPlugin

authPlugin) { this.authPlugin = authPlugin; } diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index cf5a3f79..c3126f16 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -5,6 +5,19 @@ # You can access the newest config here: # https://github.com/games647/FastLogin/blob/master/core/src/main/resources/config.yml +# This a **very** simple anti bot protection. Recommendation is to use a a dedicated program to approach this +# problem. Low level firewalls like iptables, ufw are more efficient than a Minecraft plugin. TCP reverse +# proxies could also be used and offload some work even to different host. +# +# The settings wil limit how many connections this plugin will handle. After hitting this limit. FastLogin will +# completely ignore incoming connections. Effectively there will be no database requests and network requests. +# Therefore auto logins won't be possible. +anti-bot: + # Total number of connections + connections: 200 + # Amount of time after the first connection will expire and made available + expire: 5 + # Request a premium login without forcing the player to type a command # # If you activate autoRegister, this plugin will check/do these points on login: diff --git a/core/src/test/java/com/github/games647/fastlogin/core/RateLimiterTest.java b/core/src/test/java/com/github/games647/fastlogin/core/RateLimiterTest.java new file mode 100644 index 00000000..9076b925 --- /dev/null +++ b/core/src/test/java/com/github/games647/fastlogin/core/RateLimiterTest.java @@ -0,0 +1,67 @@ +package com.github.games647.fastlogin.core; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class RateLimiterTest { + + private static final long THRESHOLD_MILLI = 10; + + /** + * Always expired + */ + @Test + public void allowExpire() throws InterruptedException { + int size = 3; + + // run twice the size to fill it first and then test it + RateLimiter rateLimiter = new RateLimiter(size, 0); + for (int i = 0; i < size; i++) { + assertTrue("Filling up", rateLimiter.tryAcquire()); + } + + for (int i = 0; i < size; i++) { + Thread.sleep(1); + assertTrue("Should be expired", rateLimiter.tryAcquire()); + } + } + + /** + * Too many requests + */ + @Test + public void shoudBlock() { + int size = 3; + + // fill the size + RateLimiter rateLimiter = new RateLimiter(size, TimeUnit.SECONDS.toMillis(30)); + for (int i = 0; i < size; i++) { + assertTrue("Filling up", rateLimiter.tryAcquire()); + } + + assertFalse("Should be full and no entry should be expired", rateLimiter.tryAcquire()); + } + + /** + * Blocked attempts shouldn't replace existing ones. + */ + @Test + public void blockedNotAdded() throws InterruptedException { + // fill the size - 100ms should be reasonable high + RateLimiter rateLimiter = new RateLimiter(1, 100); + assertTrue("Filling up", rateLimiter.tryAcquire()); + + Thread.sleep(50); + + // still is full - should fail + assertFalse("Expired too early", rateLimiter.tryAcquire()); + + // wait the remaining time and add a threshold, because + Thread.sleep(50 + THRESHOLD_MILLI); + assertTrue("Request not released", rateLimiter.tryAcquire()); + } +}