Perform protocollib checks async/non-blocking

This commit is contained in:
games647
2016-06-14 19:36:34 +02:00
parent 92c9ab5b76
commit 4800a88886
10 changed files with 376 additions and 286 deletions

View File

@ -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

View File

@ -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<K, V> {
private CacheBuilder<K, V> builder;
public class CompatibleCacheBuilder<K, V> {
private static Method BUILD_METHOD;
private static Method AS_MAP_METHOD;
@SuppressWarnings("unchecked")
private CacheBuilder() {
builder = (CacheBuilder<K, V>) CacheBuilder.newBuilder();
}
/**
* Construct a new safe cache builder.
@ -31,8 +26,15 @@ public class CacheBuilder<K, V> {
*
* @return A new cache builder.
*/
public static <K, V> CacheBuilder<K, V> newBuilder() {
return new CacheBuilder<K, V>();
public static <K, V> CompatibleCacheBuilder<K, V> newBuilder() {
return new CompatibleCacheBuilder<K, V>();
}
private CacheBuilder<K, V> builder;
@SuppressWarnings("unchecked")
private CompatibleCacheBuilder() {
builder = (CacheBuilder<K, V>) CacheBuilder.newBuilder();
}
/**
@ -56,7 +58,7 @@ public class CacheBuilder<K, V> {
* @throws IllegalArgumentException if {@code concurrencyLevel} is nonpositive
* @throws IllegalStateException if a concurrency level was already set
*/
public CacheBuilder<K, V> concurrencyLevel(int concurrencyLevel) {
public CompatibleCacheBuilder<K, V> concurrencyLevel(int concurrencyLevel) {
builder.concurrencyLevel(concurrencyLevel);
return this;
}
@ -84,7 +86,7 @@ public class CacheBuilder<K, V> {
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the time to idle or time to live was already set
*/
public CacheBuilder<K, V> expireAfterAccess(long duration, TimeUnit unit) {
public CompatibleCacheBuilder<K, V> expireAfterAccess(long duration, TimeUnit unit) {
builder.expireAfterAccess(duration, unit);
return this;
}
@ -110,7 +112,7 @@ public class CacheBuilder<K, V> {
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the time to live or time to idle was already set
*/
public CacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) {
public CompatibleCacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) {
builder.expireAfterWrite(duration, unit);
return this;
}
@ -127,7 +129,7 @@ public class CacheBuilder<K, V> {
* @throws IllegalArgumentException if {@code initialCapacity} is negative
* @throws IllegalStateException if an initial capacity was already set
*/
public CacheBuilder<K, V> initialCapacity(int initialCapacity) {
public CompatibleCacheBuilder<K, V> initialCapacity(int initialCapacity) {
builder.initialCapacity(initialCapacity);
return this;
}
@ -149,7 +151,7 @@ public class CacheBuilder<K, V> {
* @throws IllegalArgumentException if {@code size} is negative
* @throws IllegalStateException if a maximum size was already set
*/
public CacheBuilder<K, V> maximumSize(int size) {
public CompatibleCacheBuilder<K, V> maximumSize(int size) {
builder.maximumSize(size);
return this;
}
@ -186,9 +188,9 @@ public class CacheBuilder<K, V> {
* @throws IllegalStateException if a removal listener was already set
*/
@SuppressWarnings("unchecked")
public <K1 extends K, V1 extends V> CacheBuilder<K1, V1> removalListener(RemovalListener<? super K1, ? super V1> listener) {
public <K1 extends K, V1 extends V> CompatibleCacheBuilder<K1, V1> removalListener(RemovalListener<? super K1, ? super V1> listener) {
builder.removalListener(listener);
return (CacheBuilder<K1, V1>) this;
return (CompatibleCacheBuilder<K1, V1>) this;
}
/**
@ -204,7 +206,7 @@ public class CacheBuilder<K, V> {
*
* @throws IllegalStateException if a ticker was already set
*/
public CacheBuilder<K, V> ticker(Ticker ticker) {
public CompatibleCacheBuilder<K, V> ticker(Ticker ticker) {
builder.ticker(ticker);
return this;
}
@ -228,7 +230,7 @@ public class CacheBuilder<K, V> {
*
* @throws IllegalStateException if the value strength was already set
*/
public CacheBuilder<K, V> softValues() {
public CompatibleCacheBuilder<K, V> softValues() {
builder.softValues();
return this;
}
@ -245,7 +247,7 @@ public class CacheBuilder<K, V> {
*
* @throws IllegalStateException if the key strength was already set
*/
public CacheBuilder<K, V> weakKeys() {
public CompatibleCacheBuilder<K, V> weakKeys() {
builder.weakKeys();
return this;
}
@ -266,7 +268,7 @@ public class CacheBuilder<K, V> {
*
* @throws IllegalStateException if the value strength was already set
*/
public CacheBuilder<K, V> weakValues() {
public CompatibleCacheBuilder<K, V> weakValues() {
builder.weakValues();
return this;
}

View File

@ -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<String, BukkitLoginSession> session = CacheBuilder.<String, BukkitLoginSession>newBuilder()
private final ConcurrentMap<String, BukkitLoginSession> session = CompatibleCacheBuilder
.<String, BukkitLoginSession>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);

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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")) {

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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"));
}
}
}

View File

@ -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;