mirror of
https://github.com/TuxCoding/FastLogin.git
synced 2025-07-29 18:27:36 +02:00
@ -70,9 +70,9 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
|
||||
}
|
||||
|
||||
if (pluginManager.isPluginEnabled("ProtocolSupport")) {
|
||||
pluginManager.registerEvents(new ProtocolSupportListener(this), this);
|
||||
pluginManager.registerEvents(new ProtocolSupportListener(this, core.getRateLimiter()), this);
|
||||
} else if (pluginManager.isPluginEnabled("ProtocolLib")) {
|
||||
ProtocolLibListener.register(this);
|
||||
ProtocolLibListener.register(this, core.getRateLimiter());
|
||||
pluginManager.registerEvents(new SkinApplyListener(this), this);
|
||||
} else {
|
||||
logger.warn("Either ProtocolLib or ProtocolSupport have to be installed if you don't use BungeeCord");
|
||||
|
@ -6,6 +6,7 @@ import com.comphenix.protocol.events.PacketAdapter;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.core.RateLimiter;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.SecureRandom;
|
||||
@ -24,8 +25,9 @@ public class ProtocolLibListener extends PacketAdapter {
|
||||
//just create a new once on plugin enable. This used for verify token generation
|
||||
private final SecureRandom random = new SecureRandom();
|
||||
private final KeyPair keyPair = EncryptionUtil.generateKeyPair();
|
||||
private final RateLimiter rateLimiter;
|
||||
|
||||
public ProtocolLibListener(FastLoginBukkit plugin) {
|
||||
public ProtocolLibListener(FastLoginBukkit plugin, RateLimiter rateLimiter) {
|
||||
//run async in order to not block the server, because we are making api calls to Mojang
|
||||
super(params()
|
||||
.plugin(plugin)
|
||||
@ -33,13 +35,14 @@ public class ProtocolLibListener extends PacketAdapter {
|
||||
.optionAsync());
|
||||
|
||||
this.plugin = plugin;
|
||||
this.rateLimiter = rateLimiter;
|
||||
}
|
||||
|
||||
public static void register(FastLoginBukkit plugin) {
|
||||
public static void register(FastLoginBukkit plugin, RateLimiter rateLimiter) {
|
||||
//they will be created with a static builder, because otherwise it will throw a NoClassDefFoundError
|
||||
ProtocolLibrary.getProtocolManager()
|
||||
.getAsynchronousManager()
|
||||
.registerAsyncHandler(new ProtocolLibListener(plugin))
|
||||
.registerAsyncHandler(new ProtocolLibListener(plugin, rateLimiter))
|
||||
.start(WORKER_THREADS);
|
||||
}
|
||||
|
||||
@ -54,6 +57,11 @@ public class ProtocolLibListener extends PacketAdapter {
|
||||
Player sender = packetEvent.getPlayer();
|
||||
PacketType packetType = packetEvent.getPacketType();
|
||||
if (packetType == START) {
|
||||
if (!rateLimiter.tryAcquire()) {
|
||||
plugin.getLog().warn("Rate Limit hit - Ignoring player {}", sender);
|
||||
return;
|
||||
}
|
||||
|
||||
onLogin(packetEvent, sender);
|
||||
} else {
|
||||
onEncryptionBegin(packetEvent, sender);
|
||||
|
@ -4,6 +4,7 @@ import com.github.games647.craftapi.UUIDAdapter;
|
||||
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
|
||||
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
|
||||
import com.github.games647.fastlogin.bukkit.event.BukkitFastLoginPreLoginEvent;
|
||||
import com.github.games647.fastlogin.core.RateLimiter;
|
||||
import com.github.games647.fastlogin.core.StoredProfile;
|
||||
import com.github.games647.fastlogin.core.shared.JoinManagement;
|
||||
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
|
||||
@ -23,11 +24,13 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
|
||||
implements Listener {
|
||||
|
||||
private final FastLoginBukkit plugin;
|
||||
private final RateLimiter rateLimiter;
|
||||
|
||||
public ProtocolSupportListener(FastLoginBukkit plugin) {
|
||||
public ProtocolSupportListener(FastLoginBukkit plugin, RateLimiter rateLimiter) {
|
||||
super(plugin.getCore(), plugin.getCore().getAuthPluginHook());
|
||||
|
||||
this.plugin = plugin;
|
||||
this.rateLimiter = rateLimiter;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@ -36,6 +39,11 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rateLimiter.tryAcquire()) {
|
||||
plugin.getLog().warn("Rate Limit hit - Ignoring player {}", loginStartEvent.getConnection());
|
||||
return;
|
||||
}
|
||||
|
||||
String username = loginStartEvent.getConnection().getProfile().getName();
|
||||
InetSocketAddress address = loginStartEvent.getAddress();
|
||||
|
||||
|
@ -53,7 +53,8 @@ public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSen
|
||||
}
|
||||
|
||||
//events
|
||||
getProxy().getPluginManager().registerListener(this, new ConnectListener(this));
|
||||
ConnectListener connectListener = new ConnectListener(this, core.getRateLimiter());
|
||||
getProxy().getPluginManager().registerListener(this, connectListener);
|
||||
getProxy().getPluginManager().registerListener(this, new PluginMessageListener(this));
|
||||
|
||||
//this is required to listen to incoming messages from the server
|
||||
|
@ -1,9 +1,11 @@
|
||||
package com.github.games647.fastlogin.bungee.listener;
|
||||
|
||||
import com.github.games647.craftapi.UUIDAdapter;
|
||||
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
|
||||
import com.github.games647.fastlogin.bungee.FastLoginBungee;
|
||||
import com.github.games647.fastlogin.bungee.task.AsyncPremiumCheck;
|
||||
import com.github.games647.fastlogin.bungee.task.ForceLoginTask;
|
||||
import com.github.games647.fastlogin.core.RateLimiter;
|
||||
import com.github.games647.fastlogin.core.StoredProfile;
|
||||
import com.github.games647.fastlogin.core.shared.LoginSession;
|
||||
|
||||
@ -33,8 +35,11 @@ public class ConnectListener implements Listener {
|
||||
private final FastLoginBungee plugin;
|
||||
private final Property[] emptyProperties = {};
|
||||
|
||||
public ConnectListener(FastLoginBungee plugin) {
|
||||
private final RateLimiter rateLimiter;
|
||||
|
||||
public ConnectListener(FastLoginBungee plugin, RateLimiter rateLimiter) {
|
||||
this.plugin = plugin;
|
||||
this.rateLimiter = rateLimiter;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@ -43,9 +48,14 @@ public class ConnectListener implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
PendingConnection connection = preLoginEvent.getConnection();
|
||||
if (!rateLimiter.tryAcquire()) {
|
||||
plugin.getLog().warn("Rate Limit hit - Ignoring player {}", connection);
|
||||
return;
|
||||
}
|
||||
|
||||
preLoginEvent.registerIntent(plugin);
|
||||
|
||||
PendingConnection connection = preLoginEvent.getConnection();
|
||||
Runnable asyncPremiumCheck = new AsyncPremiumCheck(plugin, preLoginEvent, connection);
|
||||
plugin.getScheduler().runAsync(asyncPremiumCheck);
|
||||
}
|
||||
@ -100,10 +110,15 @@ public class ConnectListener implements Listener {
|
||||
ProxiedPlayer player = serverConnectedEvent.getPlayer();
|
||||
Server server = serverConnectedEvent.getServer();
|
||||
|
||||
BungeeLoginSession session = plugin.getSession().get(player.getPendingConnection());
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// delay sending force command, because Paper will process the login event asynchronously
|
||||
// In this case it means that the force command (plugin message) is already received and processed while
|
||||
// player is still in the login phase and reported to be offline.
|
||||
Runnable loginTask = new ForceLoginTask(plugin.getCore(), player, server);
|
||||
Runnable loginTask = new ForceLoginTask(plugin.getCore(), player, server, session);
|
||||
plugin.getScheduler().runAsync(loginTask);
|
||||
}
|
||||
|
||||
|
@ -10,10 +10,10 @@ import com.github.games647.fastlogin.core.message.LoginActionMessage.Type;
|
||||
import com.github.games647.fastlogin.core.shared.FastLoginCore;
|
||||
import com.github.games647.fastlogin.core.shared.ForceLoginManagement;
|
||||
import com.github.games647.fastlogin.core.shared.LoginSession;
|
||||
import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
@ -25,8 +25,8 @@ public class ForceLoginTask
|
||||
private final Server server;
|
||||
|
||||
public ForceLoginTask(FastLoginCore<ProxiedPlayer, CommandSender, FastLoginBungee> 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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<P extends C, C, T extends PlatformPlugin<C>> {
|
||||
|
||||
private Configuration config;
|
||||
private AuthStorage storage;
|
||||
private RateLimiter rateLimiter;
|
||||
private PasswordGenerator<P> passwordGenerator = new DefaultPasswordGenerator<>();
|
||||
private AuthPlugin<P> authPlugin;
|
||||
|
||||
@ -82,8 +84,12 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
|
||||
});
|
||||
} 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<Proxy> proxies = config.getStringList("proxies")
|
||||
.stream()
|
||||
.map(HostAndPort::fromString)
|
||||
@ -217,6 +223,10 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
|
||||
return authPlugin;
|
||||
}
|
||||
|
||||
public RateLimiter getRateLimiter() {
|
||||
return rateLimiter;
|
||||
}
|
||||
|
||||
public void setAuthPluginHook(AuthPlugin<P> authPlugin) {
|
||||
this.authPlugin = authPlugin;
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user