From 4800a88886a3e276fa5b6d7412b4c1388038d751 Mon Sep 17 00:00:00 2001 From: games647 Date: Tue, 14 Jun 2016 19:36:34 +0200 Subject: [PATCH] Perform protocollib checks async/non-blocking --- CHANGELOG.md | 3 + ...ilder.java => CompatibleCacheBuilder.java} | 42 +++-- .../fastlogin/bukkit/FastLoginBukkit.java | 24 ++- .../listener/packet/StartPacketListener.java | 174 ------------------ .../protocollib/EncryptionPacketListener.java | 61 ++++++ .../LoginSkinApplyListener.java | 8 +- .../listener/protocollib/NameCheckTask.java | 139 ++++++++++++++ .../protocollib/StartPacketListener.java | 80 ++++++++ .../VerifyResponseTask.java} | 129 ++++++------- .../ProtocolSupportListener.java | 2 +- 10 files changed, 376 insertions(+), 286 deletions(-) rename bukkit/src/main/java/com/github/games647/fastlogin/bukkit/{CacheBuilder.java => CompatibleCacheBuilder.java} (92%) delete mode 100644 bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/packet/StartPacketListener.java create mode 100644 bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionPacketListener.java rename bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/{packet => protocollib}/LoginSkinApplyListener.java (85%) create mode 100644 bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/NameCheckTask.java create mode 100644 bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/StartPacketListener.java rename bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/{packet/EncryptionPacketListener.java => protocollib/VerifyResponseTask.java} (62%) rename bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/{ => protocolsupport}/ProtocolSupportListener.java (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21afed2b..b7b19fad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ ######1.6 * Removed ProtocolLib as required dependency. You can use ProtocolSupport or BungeeCord as alternative +* Reduce the number of worker threads from 5 to 3 in ProtocolLib +* Process packets in ProtocolLib async/non-blocking -> better performance +* Fix error if forward skins is disabled ######1.5.2 diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/CacheBuilder.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/CompatibleCacheBuilder.java similarity index 92% rename from bukkit/src/main/java/com/github/games647/fastlogin/bukkit/CacheBuilder.java rename to bukkit/src/main/java/com/github/games647/fastlogin/bukkit/CompatibleCacheBuilder.java index fb07c667..6292f56e 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/CacheBuilder.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/CompatibleCacheBuilder.java @@ -5,23 +5,18 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import com.google.common.base.Ticker; +import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.RemovalListener; /** * Represents a Guava CacheBuilder that is compatible with both Guava 10 and 13 */ -public class CacheBuilder { - - private CacheBuilder builder; +public class CompatibleCacheBuilder { private static Method BUILD_METHOD; private static Method AS_MAP_METHOD; - @SuppressWarnings("unchecked") - private CacheBuilder() { - builder = (CacheBuilder) CacheBuilder.newBuilder(); - } /** * Construct a new safe cache builder. @@ -31,8 +26,15 @@ public class CacheBuilder { * * @return A new cache builder. */ - public static CacheBuilder newBuilder() { - return new CacheBuilder(); + public static CompatibleCacheBuilder newBuilder() { + return new CompatibleCacheBuilder(); + } + + private CacheBuilder builder; + + @SuppressWarnings("unchecked") + private CompatibleCacheBuilder() { + builder = (CacheBuilder) CacheBuilder.newBuilder(); } /** @@ -56,7 +58,7 @@ public class CacheBuilder { * @throws IllegalArgumentException if {@code concurrencyLevel} is nonpositive * @throws IllegalStateException if a concurrency level was already set */ - public CacheBuilder concurrencyLevel(int concurrencyLevel) { + public CompatibleCacheBuilder concurrencyLevel(int concurrencyLevel) { builder.concurrencyLevel(concurrencyLevel); return this; } @@ -84,7 +86,7 @@ public class CacheBuilder { * @throws IllegalArgumentException if {@code duration} is negative * @throws IllegalStateException if the time to idle or time to live was already set */ - public CacheBuilder expireAfterAccess(long duration, TimeUnit unit) { + public CompatibleCacheBuilder expireAfterAccess(long duration, TimeUnit unit) { builder.expireAfterAccess(duration, unit); return this; } @@ -110,7 +112,7 @@ public class CacheBuilder { * @throws IllegalArgumentException if {@code duration} is negative * @throws IllegalStateException if the time to live or time to idle was already set */ - public CacheBuilder expireAfterWrite(long duration, TimeUnit unit) { + public CompatibleCacheBuilder expireAfterWrite(long duration, TimeUnit unit) { builder.expireAfterWrite(duration, unit); return this; } @@ -127,7 +129,7 @@ public class CacheBuilder { * @throws IllegalArgumentException if {@code initialCapacity} is negative * @throws IllegalStateException if an initial capacity was already set */ - public CacheBuilder initialCapacity(int initialCapacity) { + public CompatibleCacheBuilder initialCapacity(int initialCapacity) { builder.initialCapacity(initialCapacity); return this; } @@ -149,7 +151,7 @@ public class CacheBuilder { * @throws IllegalArgumentException if {@code size} is negative * @throws IllegalStateException if a maximum size was already set */ - public CacheBuilder maximumSize(int size) { + public CompatibleCacheBuilder maximumSize(int size) { builder.maximumSize(size); return this; } @@ -186,9 +188,9 @@ public class CacheBuilder { * @throws IllegalStateException if a removal listener was already set */ @SuppressWarnings("unchecked") - public CacheBuilder removalListener(RemovalListener listener) { + public CompatibleCacheBuilder removalListener(RemovalListener listener) { builder.removalListener(listener); - return (CacheBuilder) this; + return (CompatibleCacheBuilder) this; } /** @@ -204,7 +206,7 @@ public class CacheBuilder { * * @throws IllegalStateException if a ticker was already set */ - public CacheBuilder ticker(Ticker ticker) { + public CompatibleCacheBuilder ticker(Ticker ticker) { builder.ticker(ticker); return this; } @@ -228,7 +230,7 @@ public class CacheBuilder { * * @throws IllegalStateException if the value strength was already set */ - public CacheBuilder softValues() { + public CompatibleCacheBuilder softValues() { builder.softValues(); return this; } @@ -245,7 +247,7 @@ public class CacheBuilder { * * @throws IllegalStateException if the key strength was already set */ - public CacheBuilder weakKeys() { + public CompatibleCacheBuilder weakKeys() { builder.weakKeys(); return this; } @@ -266,7 +268,7 @@ public class CacheBuilder { * * @throws IllegalStateException if the value strength was already set */ - public CacheBuilder weakValues() { + public CompatibleCacheBuilder weakValues() { builder.weakValues(); return this; } 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 e90a1d97..37284a45 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 @@ -1,19 +1,18 @@ package com.github.games647.fastlogin.bukkit; -import com.github.games647.fastlogin.bukkit.tasks.DelayedAuthHook; import com.avaje.ebeaninternal.api.ClassUtil; import com.comphenix.protocol.AsynchronousManager; import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.ProtocolManager; import com.github.games647.fastlogin.bukkit.commands.CrackedCommand; import com.github.games647.fastlogin.bukkit.commands.PremiumCommand; import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin; import com.github.games647.fastlogin.bukkit.listener.BukkitJoinListener; import com.github.games647.fastlogin.bukkit.listener.BungeeCordListener; -import com.github.games647.fastlogin.bukkit.listener.ProtocolSupportListener; -import com.github.games647.fastlogin.bukkit.listener.packet.LoginSkinApplyListener; -import com.github.games647.fastlogin.bukkit.listener.packet.EncryptionPacketListener; -import com.github.games647.fastlogin.bukkit.listener.packet.StartPacketListener; +import com.github.games647.fastlogin.bukkit.listener.protocollib.EncryptionPacketListener; +import com.github.games647.fastlogin.bukkit.listener.protocollib.LoginSkinApplyListener; +import com.github.games647.fastlogin.bukkit.listener.protocollib.StartPacketListener; +import com.github.games647.fastlogin.bukkit.listener.protocolsupport.ProtocolSupportListener; +import com.github.games647.fastlogin.bukkit.tasks.DelayedAuthHook; import com.github.games647.fastlogin.core.FastLoginCore; import com.google.common.cache.CacheLoader; @@ -30,7 +29,7 @@ import org.bukkit.plugin.java.JavaPlugin; */ public class FastLoginBukkit extends JavaPlugin { - private static final int WORKER_THREADS = 5; + private static final int WORKER_THREADS = 3; //provide a immutable key pair to be thread safe | used for encrypting and decrypting traffic private final KeyPair keyPair = EncryptionUtil.generateKeyPair(); @@ -41,7 +40,8 @@ public class FastLoginBukkit extends JavaPlugin { //this map is thread-safe for async access (Packet Listener) //SafeCacheBuilder is used in order to be version independent - private final ConcurrentMap session = CacheBuilder.newBuilder() + private final ConcurrentMap session = CompatibleCacheBuilder + .newBuilder() //2 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang) .expireAfterWrite(1, TimeUnit.MINUTES) //mapped by ip:port -> PlayerSession @@ -104,13 +104,11 @@ public class FastLoginBukkit extends JavaPlugin { if (getServer().getPluginManager().isPluginEnabled("ProtocolSupport")) { getServer().getPluginManager().registerEvents(new ProtocolSupportListener(this), this); } else if (getServer().getPluginManager().isPluginEnabled("ProtocolLib")) { - ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager(); - //we are performing HTTP request on these so run it async (seperate from the Netty IO threads) - AsynchronousManager asynchronousManager = protocolManager.getAsynchronousManager(); + AsynchronousManager asynchronousManager = ProtocolLibrary.getProtocolManager().getAsynchronousManager(); - StartPacketListener startPacketListener = new StartPacketListener(this, protocolManager); - EncryptionPacketListener encryptionPacketListener = new EncryptionPacketListener(this, protocolManager); + StartPacketListener startPacketListener = new StartPacketListener(this); + EncryptionPacketListener encryptionPacketListener = new EncryptionPacketListener(this); asynchronousManager.registerAsyncHandler(startPacketListener).start(WORKER_THREADS); asynchronousManager.registerAsyncHandler(encryptionPacketListener).start(WORKER_THREADS); diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/packet/StartPacketListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/packet/StartPacketListener.java deleted file mode 100644 index 073f1032..00000000 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/packet/StartPacketListener.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.github.games647.fastlogin.bukkit.listener.packet; - -import com.comphenix.protocol.PacketType; -import com.comphenix.protocol.ProtocolManager; -import com.comphenix.protocol.events.PacketAdapter; -import com.comphenix.protocol.events.PacketContainer; -import com.comphenix.protocol.events.PacketEvent; -import com.github.games647.fastlogin.bukkit.BukkitLoginSession; -import com.github.games647.fastlogin.bukkit.FastLoginBukkit; -import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin; -import com.github.games647.fastlogin.core.PlayerProfile; - -import java.lang.reflect.InvocationTargetException; -import java.security.PublicKey; -import java.util.Random; -import java.util.UUID; -import java.util.logging.Level; - -import org.bukkit.entity.Player; - -/** - * Handles incoming start packets from connecting clients. It - * checks if we can start checking if the player is premium and - * start a request to the client that it should start online mode - * login. - * - * Receiving packet information: - * http://wiki.vg/Protocol#Login_Start - * - * String=Username - */ -public class StartPacketListener extends PacketAdapter { - - private static final int VERIFY_TOKEN_LENGTH = 4; - - private final ProtocolManager protocolManager; - //hides the inherit Plugin plugin field, but we need a more detailed type than just Plugin - private final FastLoginBukkit plugin; - - //just create a new once on plugin enable. This used for verify token generation - private final Random random = new Random(); - - public StartPacketListener(FastLoginBukkit plugin, ProtocolManager protocolManger) { - //run async in order to not block the server, because we are making api calls to Mojang - super(params(plugin, PacketType.Login.Client.START).optionAsync()); - - this.plugin = plugin; - this.protocolManager = protocolManger; - } - - /** - * C->S : Handshake State=2 - * C->S : Login Start - * S->C : Encryption Key Request - * (Client Auth) - * C->S : Encryption Key Response - * (Server Auth, Both enable encryption) - * S->C : Login Success (*) - * - * On offline logins is Login Start followed by Login Success - */ - @Override - public void onPacketReceiving(PacketEvent packetEvent) { - plugin.setServerStarted(); - - Player player = packetEvent.getPlayer(); - - //this includes ip:port. Should be unique for an incoming login request with a timeout of 2 minutes - String sessionKey = player.getAddress().toString(); - - //remove old data every time on a new login in order to keep the session only for one person - plugin.getSessions().remove(sessionKey); - - //player.getName() won't work at this state - PacketContainer packet = packetEvent.getPacket(); - - String username = packet.getGameProfiles().read(0).getName(); - plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting to the server" - , new Object[]{sessionKey, username}); - - BukkitAuthPlugin authPlugin = plugin.getAuthPlugin(); - if (authPlugin == null) { - return; - } - - PlayerProfile profile = plugin.getCore().getStorage().loadProfile(username); - if (profile != null) { - if (profile.getUserId() == -1) { - UUID premiumUUID = null; - if (plugin.getConfig().getBoolean("nameChangeCheck") || plugin.getConfig().getBoolean("autoRegister")) { - premiumUUID = plugin.getCore().getMojangApiConnector().getPremiumUUID(username); - } - - //user not exists in the db - try { - if (premiumUUID != null && plugin.getConfig().getBoolean("nameChangeCheck")) { - profile = plugin.getCore().getStorage().loadProfile(premiumUUID); - if (profile != null) { - plugin.getLogger().log(Level.FINER, "Player {0} changed it's username", premiumUUID); - enablePremiumLogin(username, profile, sessionKey, player, packetEvent, false); - return; - } - } - - if (premiumUUID != null - && plugin.getConfig().getBoolean("autoRegister") && !authPlugin.isRegistered(username)) { - plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username); - enablePremiumLogin(username, profile, sessionKey, player, packetEvent, false); - return; - } - - //no premium check passed so we save it as a cracked player - BukkitLoginSession loginSession = new BukkitLoginSession(username, profile); - plugin.getSessions().put(sessionKey, loginSession); - } catch (Exception ex) { - plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex); - } - } else if (profile.isPremium()) { - enablePremiumLogin(username, profile, sessionKey, player, packetEvent, true); - } else { - BukkitLoginSession loginSession = new BukkitLoginSession(username, profile); - plugin.getSessions().put(sessionKey, loginSession); - } - } - } - - //minecraft server implementation - //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161 - private void enablePremiumLogin(String username, PlayerProfile profile, String sessionKey, Player player - , PacketEvent packetEvent, boolean registered) { - //randomized server id to make sure the request is for our server - //this could be relevant http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/ - String serverId = Long.toString(random.nextLong(), 16); - - //generate a random token which should be the same when we receive it from the client - byte[] verifyToken = new byte[VERIFY_TOKEN_LENGTH]; - random.nextBytes(verifyToken); - - boolean success = sentEncryptionRequest(player, serverId, verifyToken); - if (success) { - BukkitLoginSession playerSession = new BukkitLoginSession(username, serverId - , verifyToken, registered, profile); - plugin.getSessions().put(sessionKey, playerSession); - //cancel only if the player has a paid account otherwise login as normal offline player - packetEvent.setCancelled(true); - } - } - - private boolean sentEncryptionRequest(Player player, String serverId, byte[] verifyToken) { - try { - /** - * Packet Information: http://wiki.vg/Protocol#Encryption_Request - * - * ServerID="" (String) - * key=public server key - * verifyToken=random 4 byte array - */ - PacketContainer newPacket = protocolManager.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN); - - newPacket.getStrings().write(0, serverId); - newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getServerKey().getPublic()); - - newPacket.getByteArrays().write(0, verifyToken); - - //serverId is a empty string - protocolManager.sendServerPacket(player, newPacket); - return true; - } catch (InvocationTargetException ex) { - plugin.getLogger().log(Level.SEVERE, "Cannot send encryption packet. Falling back to normal login", ex); - } - - return false; - } -} diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionPacketListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionPacketListener.java new file mode 100644 index 00000000..74cd4fea --- /dev/null +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/EncryptionPacketListener.java @@ -0,0 +1,61 @@ +package com.github.games647.fastlogin.bukkit.listener.protocollib; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketEvent; +import com.github.games647.fastlogin.bukkit.FastLoginBukkit; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + + +/** + * Handles incoming encryption responses from connecting clients. + * It prevents them from reaching the server because that cannot handle + * it in offline mode. + * + * Moreover this manages a started premium check from + * this plugin. So check if all data is correct and we can prove him as a + * owner of a paid minecraft account. + * + * Receiving packet information: + * http://wiki.vg/Protocol#Encryption_Response + * + * sharedSecret=encrypted byte array + * verify token=encrypted byte array + */ +public class EncryptionPacketListener extends PacketAdapter { + + //hides the inherit Plugin plugin field, but we need this type + private final FastLoginBukkit plugin; + + public EncryptionPacketListener(FastLoginBukkit plugin) { + //run async in order to not block the server, because we make api calls to Mojang + super(params(plugin, PacketType.Login.Client.ENCRYPTION_BEGIN).optionAsync()); + + this.plugin = plugin; + } + + /** + * C->S : Handshake State=2 + * C->S : Login Start + * S->C : Encryption Key Request + * (Client Auth) + * C->S : Encryption Key Response + * (Server Auth, Both enable encryption) + * S->C : Login Success (*) + * + * On offline logins is Login Start followed by Login Success + * + * Minecraft Server implementation + * https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L180 + */ + @Override + public void onPacketReceiving(PacketEvent packetEvent) { + Player sender = packetEvent.getPlayer(); + byte[] sharedSecret = packetEvent.getPacket().getByteArrays().read(0); + + packetEvent.getAsyncMarker().incrementProcessingDelay(); + VerifyResponseTask verifyTask = new VerifyResponseTask(plugin, packetEvent, sender, sharedSecret); + Bukkit.getScheduler().runTaskAsynchronously(plugin, verifyTask); + } +} diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/packet/LoginSkinApplyListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/LoginSkinApplyListener.java similarity index 85% rename from bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/packet/LoginSkinApplyListener.java rename to bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/LoginSkinApplyListener.java index c4803963..080c410f 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/packet/LoginSkinApplyListener.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/LoginSkinApplyListener.java @@ -1,4 +1,4 @@ -package com.github.games647.fastlogin.bukkit.listener.packet; +package com.github.games647.fastlogin.bukkit.listener.protocollib; import com.comphenix.protocol.wrappers.WrappedGameProfile; import com.comphenix.protocol.wrappers.WrappedSignedProperty; @@ -9,7 +9,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.PlayerJoinEvent; public class LoginSkinApplyListener implements Listener { @@ -20,8 +20,8 @@ public class LoginSkinApplyListener implements Listener { } @EventHandler(priority = EventPriority.LOW) - public void onPlayerLogin(PlayerLoginEvent loginEvent) { - Player player = loginEvent.getPlayer(); + public void onPlayerLogin(PlayerJoinEvent joinEvent) { + Player player = joinEvent.getPlayer(); BukkitLoginSession session = plugin.getSessions().get(player.getAddress().toString()); if (session != null && plugin.getConfig().getBoolean("forwardSkin")) { diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/NameCheckTask.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/NameCheckTask.java new file mode 100644 index 00000000..e7e228f3 --- /dev/null +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/NameCheckTask.java @@ -0,0 +1,139 @@ +package com.github.games647.fastlogin.bukkit.listener.protocollib; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.github.games647.fastlogin.bukkit.BukkitLoginSession; +import com.github.games647.fastlogin.bukkit.FastLoginBukkit; +import com.github.games647.fastlogin.core.PlayerProfile; + +import java.lang.reflect.InvocationTargetException; +import java.security.PublicKey; +import java.util.Random; +import java.util.UUID; +import java.util.logging.Level; + +import org.bukkit.entity.Player; + +public class NameCheckTask implements Runnable { + + private static final int VERIFY_TOKEN_LENGTH = 4; + + private final FastLoginBukkit plugin; + private final PacketEvent packetEvent; + + private final Random random; + + private final Player player; + private final String username; + + public NameCheckTask(FastLoginBukkit plugin, PacketEvent packetEvent, Random random, Player player, String username) { + this.plugin = plugin; + this.packetEvent = packetEvent; + this.random = random; + this.player = player; + this.username = username; + } + + @Override + public void run() { + try { + nameCheck(); + } finally { + ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent); + } + } + + private void nameCheck() { + PlayerProfile profile = plugin.getCore().getStorage().loadProfile(username); + if (profile == null) { + return; + } + + if (profile.getUserId() == -1) { + UUID premiumUUID = null; + if (plugin.getConfig().getBoolean("nameChangeCheck") || plugin.getConfig().getBoolean("autoRegister")) { + premiumUUID = plugin.getCore().getMojangApiConnector().getPremiumUUID(username); + } + + //user not exists in the db + try { + if (premiumUUID != null && plugin.getConfig().getBoolean("nameChangeCheck")) { + profile = plugin.getCore().getStorage().loadProfile(premiumUUID); + if (profile != null) { + plugin.getLogger().log(Level.FINER, "Player {0} changed it's username", premiumUUID); + enablePremiumLogin(profile, false); + return; + } + } + + if (premiumUUID != null && plugin.getConfig().getBoolean("autoRegister") + && !plugin.getAuthPlugin().isRegistered(username)) { + plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username); + enablePremiumLogin(profile, false); + return; + } + + //no premium check passed so we save it as a cracked player + BukkitLoginSession loginSession = new BukkitLoginSession(username, profile); + plugin.getSessions().put(player.getAddress().toString(), loginSession); + } catch (Exception ex) { + plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex); + } + } else if (profile.isPremium()) { + enablePremiumLogin(profile, true); + } else { + BukkitLoginSession loginSession = new BukkitLoginSession(username, profile); + plugin.getSessions().put(player.getAddress().toString(), loginSession); + } + } + + //minecraft server implementation + //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161 + private void enablePremiumLogin(PlayerProfile profile, boolean registered) { + //randomized server id to make sure the request is for our server + //this could be relevant http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/ + String serverId = Long.toString(random.nextLong(), 16); + + //generate a random token which should be the same when we receive it from the client + byte[] verify = new byte[VERIFY_TOKEN_LENGTH]; + random.nextBytes(verify); + + boolean success = sentEncryptionRequest(player, serverId, verify); + if (success) { + BukkitLoginSession playerSession = new BukkitLoginSession(username, serverId, verify, registered, profile); + plugin.getSessions().put(player.getAddress().toString(), playerSession); + //cancel only if the player has a paid account otherwise login as normal offline player + synchronized (packetEvent.getAsyncMarker().getProcessingLock()) { + packetEvent.setCancelled(true); + } + } + } + + private boolean sentEncryptionRequest(Player player, String serverId, byte[] verifyToken) { + ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager(); + try { + /** + * Packet Information: http://wiki.vg/Protocol#Encryption_Request + * + * ServerID="" (String) key=public server key verifyToken=random 4 byte array + */ + PacketContainer newPacket = protocolManager.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN); + + newPacket.getStrings().write(0, serverId); + newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getServerKey().getPublic()); + + newPacket.getByteArrays().write(0, verifyToken); + + //serverId is a empty string + protocolManager.sendServerPacket(player, newPacket); + return true; + } catch (InvocationTargetException ex) { + plugin.getLogger().log(Level.SEVERE, "Cannot send encryption packet. Falling back to normal login", ex); + } + + return false; + } +} diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/StartPacketListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/StartPacketListener.java new file mode 100644 index 00000000..e05aec1e --- /dev/null +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/StartPacketListener.java @@ -0,0 +1,80 @@ +package com.github.games647.fastlogin.bukkit.listener.protocollib; + +import com.comphenix.protocol.PacketType; +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.bukkit.hooks.BukkitAuthPlugin; + +import java.util.Random; +import java.util.logging.Level; +import org.bukkit.Bukkit; + +import org.bukkit.entity.Player; + +/** + * Handles incoming start packets from connecting clients. It + * checks if we can start checking if the player is premium and + * start a request to the client that it should start online mode + * login. + * + * Receiving packet information: + * http://wiki.vg/Protocol#Login_Start + * + * String=Username + */ +public class StartPacketListener extends PacketAdapter { + + //hides the inherit Plugin plugin field, but we need a more detailed type than just Plugin + private final FastLoginBukkit plugin; + + //just create a new once on plugin enable. This used for verify token generation + private final Random random = new Random(); + + public StartPacketListener(FastLoginBukkit plugin) { + //run async in order to not block the server, because we are making api calls to Mojang + super(params(plugin, PacketType.Login.Client.START).optionAsync()); + + this.plugin = plugin; + } + + /** + * C->S : Handshake State=2 + * C->S : Login Start + * S->C : Encryption Key Request + * (Client Auth) + * C->S : Encryption Key Response + * (Server Auth, Both enable encryption) + * S->C : Login Success (*) + * + * On offline logins is Login Start followed by Login Success + */ + @Override + public void onPacketReceiving(PacketEvent packetEvent) { + plugin.setServerStarted(); + + Player player = packetEvent.getPlayer(); + + //this includes ip:port. Should be unique for an incoming login request with a timeout of 2 minutes + String sessionKey = player.getAddress().toString(); + + //remove old data every time on a new login in order to keep the session only for one person + plugin.getSessions().remove(sessionKey); + + //player.getName() won't work at this state + PacketContainer packet = packetEvent.getPacket(); + + String username = packet.getGameProfiles().read(0).getName(); + plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting", new Object[]{sessionKey, username}); + + BukkitAuthPlugin authPlugin = plugin.getAuthPlugin(); + if (authPlugin == null) { + return; + } + + packetEvent.getAsyncMarker().incrementProcessingDelay(); + NameCheckTask nameCheckTask = new NameCheckTask(plugin, packetEvent, random, player, username); + Bukkit.getScheduler().runTaskAsynchronously(plugin, nameCheckTask); + } +} diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/packet/EncryptionPacketListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java similarity index 62% rename from bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/packet/EncryptionPacketListener.java rename to bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java index 04669c1f..33330566 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/packet/EncryptionPacketListener.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java @@ -1,8 +1,8 @@ -package com.github.games647.fastlogin.bukkit.listener.packet; +package com.github.games647.fastlogin.bukkit.listener.protocollib; import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.ProtocolManager; -import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; @@ -26,65 +26,42 @@ import javax.crypto.SecretKey; import org.bukkit.entity.Player; -/** - * Handles incoming encryption responses from connecting clients. - * It prevents them from reaching the server because that cannot handle - * it in offline mode. - * - * Moreover this manages a started premium check from - * this plugin. So check if all data is correct and we can prove him as a - * owner of a paid minecraft account. - * - * Receiving packet information: - * http://wiki.vg/Protocol#Encryption_Response - * - * sharedSecret=encrypted byte array - * verify token=encrypted byte array - */ -public class EncryptionPacketListener extends PacketAdapter { +public class VerifyResponseTask implements Runnable { - private final ProtocolManager protocolManager; - //hides the inherit Plugin plugin field, but we need this type private final FastLoginBukkit plugin; + private final PacketEvent packetEvent; - public EncryptionPacketListener(FastLoginBukkit plugin, ProtocolManager protocolManger) { - //run async in order to not block the server, because we make api calls to Mojang - super(params(plugin, PacketType.Login.Client.ENCRYPTION_BEGIN).optionAsync()); + private final Player fromPlayer; + private final byte[] sharedSecret; + + public VerifyResponseTask(FastLoginBukkit plugin, PacketEvent packetEvent, Player fromPlayer, byte[] sharedSecret) { this.plugin = plugin; - this.protocolManager = protocolManger; + this.packetEvent = packetEvent; + this.fromPlayer = fromPlayer; + this.sharedSecret = sharedSecret; } - /** - * C->S : Handshake State=2 - * C->S : Login Start - * S->C : Encryption Key Request - * (Client Auth) - * C->S : Encryption Key Response - * (Server Auth, Both enable encryption) - * S->C : Login Success (*) - * - * On offline logins is Login Start followed by Login Success - * - * Minecraft Server implementation - * https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L180 - */ @Override - public void onPacketReceiving(PacketEvent packetEvent) { - Player player = packetEvent.getPlayer(); - - BukkitLoginSession session = plugin.getSessions().get(player.getAddress().toString()); - if (session == null) { - disconnect(packetEvent, plugin.getCore().getMessage("invalid-requst"), true - , "Player {0} tried to send encryption response at invalid state", player.getAddress()); - return; + public void run() { + try { + BukkitLoginSession session = plugin.getSessions().get(fromPlayer.getAddress().toString()); + if (session == null) { + disconnect(plugin.getCore().getMessage("invalid-requst"), true + , "Player {0} tried to send encryption response at invalid state", fromPlayer.getAddress()); + } else { + verifyResponse(session); + } + } finally { + ProtocolLibrary.getProtocolManager().getAsynchronousManager().signalPacketTransmission(packetEvent); } + } + private void verifyResponse(BukkitLoginSession session) { PrivateKey privateKey = plugin.getServerKey().getPrivate(); - byte[] sharedSecret = packetEvent.getPacket().getByteArrays().read(0); SecretKey loginKey = EncryptionUtil.decryptSharedKey(privateKey, sharedSecret); - if (!checkVerifyToken(session, privateKey, packetEvent) || !encryptConnection(player, loginKey, packetEvent)) { + if (!checkVerifyToken(session, privateKey) || !encryptConnection(loginKey)) { return; } @@ -102,34 +79,35 @@ public class EncryptionPacketListener extends PacketAdapter { plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username); session.setVerified(true); - setPremiumUUID(session, player); - receiveFakeStartPacket(username, player); + setPremiumUUID(session.getUuid()); + receiveFakeStartPacket(username); } else { //user tried to fake a authentication - disconnect(packetEvent, plugin.getCore().getMessage("invalid-session"), true + disconnect(plugin.getCore().getMessage("invalid-session"), true , "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}" - , session.getUsername(), player.getAddress(), serverId); + , session.getUsername(), fromPlayer.getAddress(), serverId); } //this is a fake packet; it shouldn't be send to the server - packetEvent.setCancelled(true); + synchronized (packetEvent.getAsyncMarker().getProcessingLock()) { + packetEvent.setCancelled(true); + } } - private void setPremiumUUID(BukkitLoginSession session, Player player) { - UUID uuid = session.getUuid(); - if (plugin.getConfig().getBoolean("premiumUuid") && uuid != null) { + private void setPremiumUUID(UUID premiumUUID) { + if (plugin.getConfig().getBoolean("premiumUuid") && premiumUUID != null) { try { - Object networkManager = getNetworkManager(player); + Object networkManager = getNetworkManager(); //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/NetworkManager.java#L69 Field spoofField = FuzzyReflection.fromObject(networkManager).getFieldByType("spoofedUUID", UUID.class); - spoofField.set(networkManager, uuid); + spoofField.set(networkManager, premiumUUID); } catch (ReflectiveOperationException reflectiveOperationException) { plugin.getLogger().log(Level.SEVERE, "Error setting premium uuid", reflectiveOperationException); } } } - private boolean checkVerifyToken(BukkitLoginSession session, PrivateKey privateKey, PacketEvent packetEvent) { + private boolean checkVerifyToken(BukkitLoginSession session, PrivateKey privateKey) { byte[] requestVerify = session.getVerifyToken(); //encrypted verify token byte[] responseVerify = packetEvent.getPacket().getByteArrays().read(1); @@ -137,7 +115,7 @@ public class EncryptionPacketListener extends PacketAdapter { //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L182 if (!Arrays.equals(requestVerify, EncryptionUtil.decryptData(privateKey, responseVerify))) { //check if the verify token are equal to the server sent one - disconnect(packetEvent, plugin.getCore().getMessage("invalid-verify-token"), true + disconnect(plugin.getCore().getMessage("invalid-verify-token"), true , "Player {0} ({1}) tried to login with an invalid verify token. Server: {2} Client: {3}" , session.getUsername(), packetEvent.getPlayer().getAddress(), requestVerify, responseVerify); return false; @@ -147,9 +125,8 @@ public class EncryptionPacketListener extends PacketAdapter { } //try to get the networkManager from ProtocolLib - private Object getNetworkManager(Player player) - throws IllegalAccessException, NoSuchFieldException { - Object socketInjector = TemporaryPlayerFactory.getInjectorFromPlayer(player); + private Object getNetworkManager() throws IllegalAccessException, NoSuchFieldException { + Object socketInjector = TemporaryPlayerFactory.getInjectorFromPlayer(fromPlayer); Field injectorField = socketInjector.getClass().getDeclaredField("injector"); injectorField.setAccessible(true); @@ -160,11 +137,10 @@ public class EncryptionPacketListener extends PacketAdapter { return injectorField.get(rawInjector); } - private boolean encryptConnection(Player player, SecretKey loginKey, PacketEvent packetEvent) - throws IllegalArgumentException { + private boolean encryptConnection(SecretKey loginKey) throws IllegalArgumentException { try { //get the NMS connection handle of this player - Object networkManager = getNetworkManager(player); + Object networkManager = getNetworkManager(); //try to detect the method by parameters Method encryptConnectionMethod = FuzzyReflection @@ -174,16 +150,15 @@ public class EncryptionPacketListener extends PacketAdapter { //the client expects this behaviour encryptConnectionMethod.invoke(networkManager, loginKey); } catch (ReflectiveOperationException ex) { - disconnect(packetEvent, plugin.getCore().getMessage("error-kick"), false, "Couldn't enable encryption", ex); + disconnect(plugin.getCore().getMessage("error-kick"), false, "Couldn't enable encryption", ex); return false; } return true; } - private void disconnect(PacketEvent packetEvent, String kickReason, boolean debugLevel, String logMessage - , Object... arguments) { - if (debugLevel) { + private void disconnect(String kickReason, boolean debug, String logMessage, Object... arguments) { + if (debug) { plugin.getLogger().log(Level.FINE, logMessage, arguments); } else { plugin.getLogger().log(Level.SEVERE, logMessage, arguments); @@ -191,10 +166,14 @@ public class EncryptionPacketListener extends PacketAdapter { kickPlayer(packetEvent.getPlayer(), kickReason); //cancel the event in order to prevent the server receiving an invalid packet - packetEvent.setCancelled(true); + synchronized (packetEvent.getAsyncMarker().getProcessingLock()) { + packetEvent.setCancelled(true); + } } private void kickPlayer(Player player, String reason) { + ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager(); + PacketContainer kickPacket = protocolManager.createPacket(PacketType.Login.Server.DISCONNECT); kickPacket.getChatComponents().write(0, WrappedChatComponent.fromText(reason)); @@ -210,7 +189,9 @@ public class EncryptionPacketListener extends PacketAdapter { } //fake a new login packet in order to let the server handle all the other stuff - private void receiveFakeStartPacket(String username, Player from) { + private void receiveFakeStartPacket(String username) { + ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager(); + //see StartPacketListener for packet information PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START); @@ -219,11 +200,11 @@ public class EncryptionPacketListener extends PacketAdapter { startPacket.getGameProfiles().write(0, fakeProfile); try { //we don't want to handle our own packets so ignore filters - protocolManager.recieveClientPacket(from, startPacket, false); + protocolManager.recieveClientPacket(fromPlayer, startPacket, false); } catch (InvocationTargetException | IllegalAccessException ex) { plugin.getLogger().log(Level.WARNING, "Failed to fake a new start packet", ex); //cancel the event in order to prevent the server receiving an invalid packet - kickPlayer(from, plugin.getCore().getMessage("error-kick")); + kickPlayer(fromPlayer, plugin.getCore().getMessage("error-kick")); } } } diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/ProtocolSupportListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocolsupport/ProtocolSupportListener.java similarity index 98% rename from bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/ProtocolSupportListener.java rename to bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocolsupport/ProtocolSupportListener.java index f2727d7b..91429fb3 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/ProtocolSupportListener.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocolsupport/ProtocolSupportListener.java @@ -1,4 +1,4 @@ -package com.github.games647.fastlogin.bukkit.listener; +package com.github.games647.fastlogin.bukkit.listener.protocolsupport; import com.github.games647.fastlogin.bukkit.BukkitLoginSession; import com.github.games647.fastlogin.bukkit.FastLoginBukkit;