diff --git a/README.md b/README.md
index 53059c80..e92630d5 100644
--- a/README.md
+++ b/README.md
@@ -9,3 +9,5 @@ Requirements:
* An auth plugin. Supported Plugins:
* [AuthMe](http://dev.bukkit.org/bukkit-plugins/authme-reloaded/)
* [xAuth](http://dev.bukkit.org/bukkit-plugins/xauth/)
+ * [CrazyLogin](http://dev.bukkit.org/bukkit-plugins/crazylogin/)
+ * [LoginSecurity](http://dev.bukkit.org/bukkit-plugins/loginsecurity/)
diff --git a/lib/CrazyCore v10.7.7.jar b/lib/CrazyCore v10.7.7.jar
new file mode 100644
index 00000000..3330015c
Binary files /dev/null and b/lib/CrazyCore v10.7.7.jar differ
diff --git a/lib/CrazyLogin v7.23.2.jar b/lib/CrazyLogin v7.23.2.jar
new file mode 100644
index 00000000..746c3216
Binary files /dev/null and b/lib/CrazyLogin v7.23.2.jar differ
diff --git a/lib/LoginSecurity v2.0.10.jar b/lib/LoginSecurity v2.0.10.jar
new file mode 100644
index 00000000..d394352f
Binary files /dev/null and b/lib/LoginSecurity v2.0.10.jar differ
diff --git a/pom.xml b/pom.xml
index 40161e20..daf4e1e7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,11 +8,11 @@
jar
FastLogin
- 0.1
+ 0.2
2015
http://dev.bukkit.org/bukkit-plugins/fastlogin
- Automatically logins premium player on a offline mode server
+ Automatically logins premium (paid accounts) player on a offline mode server
@@ -43,7 +43,6 @@
maven-compiler-plugin
3.2
-
1.8
1.8
true
@@ -110,7 +109,7 @@
org.spigotmc
- spigot
+ spigot-api
1.8.8-R0.1-SNAPSHOT
provided
@@ -119,7 +118,7 @@
com.comphenix.protocol
ProtocolLib
- 3.6.3-SNAPSHOT
+ 3.6.5-SNAPSHOT
true
@@ -134,6 +133,7 @@
de.luricos.bukkit
xAuth
2.6
+
net.gravitydevelopment.updater
@@ -145,5 +145,29 @@
+
+
+ de.st_ddt.crazy
+ CrazyCore
+ 10.7.7
+ system
+ ${project.basedir}/lib/CrazyCore v10.7.7.jar
+
+
+
+ de.st_ddt.crazy
+ CrazyLogin
+ 7.23
+ system
+ ${project.basedir}/lib/CrazyLogin v7.23.2.jar
+
+
+
+ me.lenis0012.ls
+ LoginSecurity
+ 2.0.10
+ system
+ ${project.basedir}/lib/LoginSecurity v2.0.10.jar
+
diff --git a/src/main/java/com/github/games647/fastlogin/Encryption.java b/src/main/java/com/github/games647/fastlogin/Encryption.java
new file mode 100644
index 00000000..2555d8e4
--- /dev/null
+++ b/src/main/java/com/github/games647/fastlogin/Encryption.java
@@ -0,0 +1,135 @@
+package com.github.games647.fastlogin;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Source:
+ * https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/MinecraftEncryption.java
+ *
+ * Remapped by:
+ * https://github.com/Techcable/MinecraftMappings/tree/master/1.8
+ */
+public class Encryption {
+
+ public static KeyPair generateKeyPair() {
+ try {
+ KeyPairGenerator keypairgenerator = KeyPairGenerator.getInstance("RSA");
+
+ keypairgenerator.initialize(1024);
+ return keypairgenerator.generateKeyPair();
+ } catch (NoSuchAlgorithmException nosuchalgorithmexception) {
+ //Should be existing in every vm
+ return null;
+ }
+ }
+
+ public static byte[] getServerIdHash(String serverId, PublicKey publickey, SecretKey secretkey) {
+ try {
+ return digestOperation("SHA-1"
+ , new byte[][]{serverId.getBytes("ISO_8859_1"), secretkey.getEncoded(), publickey.getEncoded()});
+ } catch (UnsupportedEncodingException unsupportedencodingexception) {
+ unsupportedencodingexception.printStackTrace();
+ return null;
+ }
+ }
+
+ private static byte[] digestOperation(String algo, byte[]... content) {
+ try {
+ MessageDigest messagedigest = MessageDigest.getInstance(algo);
+ int dataLength = content.length;
+
+ for (int i = 0; i < dataLength; ++i) {
+ byte[] abyte1 = content[i];
+
+ messagedigest.update(abyte1);
+ }
+
+ return messagedigest.digest();
+ } catch (NoSuchAlgorithmException nosuchalgorithmexception) {
+ nosuchalgorithmexception.printStackTrace();
+ return null;
+ }
+ }
+
+ public static PublicKey decodePublicKey(byte[] encodedKey) {
+ try {
+ X509EncodedKeySpec x509encodedkeyspec = new X509EncodedKeySpec(encodedKey);
+ KeyFactory keyfactory = KeyFactory.getInstance("RSA");
+
+ return keyfactory.generatePublic(x509encodedkeyspec);
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException nosuchalgorithmexception) {
+ ;
+ }
+
+ System.err.println("Public key reconstitute failed!");
+ return null;
+ }
+
+ public static SecretKey decryptSharedKey(PrivateKey privatekey, byte[] encryptedSharedKey) {
+ return new SecretKeySpec(decryptData(privatekey, encryptedSharedKey), "AES");
+ }
+
+ public static byte[] decryptData(Key key, byte[] abyte) {
+ return cipherOperation(Cipher.DECRYPT_MODE, key, abyte);
+ }
+
+ private static byte[] cipherOperation(int operationMode, Key key, byte[] data) {
+ try {
+ return createCipherInstance(operationMode, key.getAlgorithm(), key).doFinal(data);
+ } catch (IllegalBlockSizeException | BadPaddingException illegalblocksizeexception) {
+ illegalblocksizeexception.printStackTrace();
+ }
+
+ System.err.println("Cipher data failed!");
+ return null;
+ }
+
+ private static Cipher createCipherInstance(int operationMode, String cipherName, Key key) {
+ try {
+ Cipher cipher = Cipher.getInstance(cipherName);
+
+ cipher.init(operationMode, key);
+ return cipher;
+ } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException invalidkeyexception) {
+ invalidkeyexception.printStackTrace();
+ }
+
+ System.err.println("Cipher creation failed!");
+ return null;
+ }
+
+ public static Cipher createBufferedBlockCipher(int operationMode, Key key) {
+ try {
+ Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
+
+ cipher.init(operationMode, key, new IvParameterSpec(key.getEncoded()));
+ return cipher;
+ } catch (GeneralSecurityException generalsecurityexception) {
+ throw new RuntimeException(generalsecurityexception);
+ }
+ }
+
+ private Encryption() {
+ //utility
+ }
+}
diff --git a/src/main/java/com/github/games647/fastlogin/FastLogin.java b/src/main/java/com/github/games647/fastlogin/FastLogin.java
index 4b6f7a2c..c2903e4c 100644
--- a/src/main/java/com/github/games647/fastlogin/FastLogin.java
+++ b/src/main/java/com/github/games647/fastlogin/FastLogin.java
@@ -3,91 +3,144 @@ package com.github.games647.fastlogin;
import com.github.games647.fastlogin.listener.PlayerListener;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
+import com.comphenix.protocol.utility.SafeCacheBuilder;
+import com.github.games647.fastlogin.hooks.AuthPlugin;
import com.github.games647.fastlogin.listener.EncryptionPacketListener;
import com.github.games647.fastlogin.listener.StartPacketListener;
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.reflect.ClassPath;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
-
import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
import org.bukkit.plugin.java.JavaPlugin;
public class FastLogin extends JavaPlugin {
- private final KeyPair keyPair = generateKey();
- private final Cache session = CacheBuilder.newBuilder()
+ private static final int TIMEOUT = 15000;
+ private static final String USER_AGENT = "Premium-Checker";
+
+ //provide a immutable key pair to be thread safe
+ private final KeyPair keyPair = Encryption.generateKeyPair();
+ //this map is thread-safe for async access (Packet Listener)
+ //SafeCacheBuilder is used in order to be version independent
+ private final ConcurrentMap session = SafeCacheBuilder.newBuilder()
+ //mapped by ip:port
.expireAfterWrite(2, TimeUnit.MINUTES)
- .build();
+ //2 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
+ .build(new CacheLoader() {
- @Override
- public void onEnable() {
- if (!isEnabled()) {
- return;
- }
-
- if (!getServer().getPluginManager().isPluginEnabled("AuthMe")
- && !getServer().getPluginManager().isPluginEnabled("xAuth")) {
- getLogger().warning("No support offline Auth plugin found. ");
- getLogger().warning("Disabling this plugin...");
-
- setEnabled(false);
- return;
- }
-
- ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
- protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager));
- protocolManager.addPacketListener(new StartPacketListener(this, protocolManager));
-
- getServer().getPluginManager().registerEvents(new PlayerListener(this), this);
- }
+ @Override
+ public PlayerSession load(String key) throws Exception {
+ //A key should be inserted manually on start packet
+ throw new UnsupportedOperationException("Not supported");
+ }
+ });
@Override
public void onLoad() {
- //online mode is only changeable aftter a restart
+ //online mode is only changeable after a restart so check it here
if (getServer().getOnlineMode()) {
getLogger().severe("Server have to be in offline mode");
setEnabled(false);
}
-
- generateKey();
}
- private KeyPair generateKey() {
- try {
- KeyPairGenerator keypairgenerator = KeyPairGenerator.getInstance("RSA");
-
- keypairgenerator.initialize(1024);
- return keypairgenerator.generateKeyPair();
- } catch (NoSuchAlgorithmException noSuchAlgorithmException) {
- //Should be default existing in every vm
+ @Override
+ public void onEnable() {
+ if (!isEnabled() || !registerHooks()) {
+ return;
}
- return null;
+ //register packet listeners on success
+ ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
+ protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager));
+ protocolManager.addPacketListener(new StartPacketListener(this, protocolManager));
}
- public Cache getSession() {
+ @Override
+ public void onDisable() {
+ //clean up
+ session.clear();
+ }
+
+ /**
+ * Gets a thread-safe map about players which are connecting
+ * to the server are being checked to be premium (paid account)
+ *
+ * @return a thread-safe session map
+ */
+ public ConcurrentMap getSessions() {
return session;
}
+ /**
+ * Gets the server KeyPair
+ *
+ * @return the server KeyPair
+ */
public KeyPair getKeyPair() {
return keyPair;
}
+ /**
+ * Prepares a Mojang API connection. The connection is not
+ * started in this method
+ *
+ * @param url the url connecting to
+ * @return the prepared connection
+ *
+ * @throws IOException on invalid url format or on {@link java.net.URL#openConnection() }
+ */
public HttpURLConnection getConnection(String url) throws IOException {
- final HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
- connection.setConnectTimeout(15000);
- connection.setReadTimeout(15000);
+ HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+ connection.setConnectTimeout(TIMEOUT);
+ connection.setReadTimeout(TIMEOUT);
+ //the new Mojang API just uses json as response
connection.setRequestProperty("Content-Type", "application/json");
- connection.setRequestProperty("User-Agent", "Premium-Checker");
+ connection.setRequestProperty("User-Agent", USER_AGENT);
return connection;
}
+
+ private boolean registerHooks() {
+ AuthPlugin authPluginHook = null;
+ try {
+ String hooksPackage = this.getClass().getPackage().getName() + ".hooks";
+ //Look through all classes in the hooks package and look for supporting plugins on the server
+ for (ClassPath.ClassInfo clazzInfo : ClassPath.from(getClassLoader()).getTopLevelClasses(hooksPackage)) {
+ //remove the hook suffix
+ String pluginName = clazzInfo.getSimpleName().replace("Hook", "");
+ Class> clazz = clazzInfo.load();
+ //uses only member classes which uses AuthPlugin interface (skip interfaces)
+ if (AuthPlugin.class.isAssignableFrom(clazz)
+ && getServer().getPluginManager().isPluginEnabled(pluginName)) {
+ authPluginHook = (AuthPlugin) clazz.newInstance();
+ getLogger().log(Level.INFO, "Hooking into auth plugin: {0}", pluginName);
+ break;
+ }
+ }
+ } catch (InstantiationException | IllegalAccessException | IOException ex) {
+ getLogger().log(Level.SEVERE, "Couldn't load the integration class", ex);
+ }
+
+ if (authPluginHook == null) {
+ //run this check for exceptions and not found plugins
+ getLogger().warning("No support offline Auth plugin found. ");
+ getLogger().warning("Disabling this plugin...");
+
+ setEnabled(false);
+ return false;
+ }
+
+ //We found a supporting plugin - we can now register a forwarding listener
+ getServer().getPluginManager().registerEvents(new PlayerListener(this, authPluginHook), this);
+ return true;
+ }
}
diff --git a/src/main/java/com/github/games647/fastlogin/PlayerData.java b/src/main/java/com/github/games647/fastlogin/PlayerData.java
deleted file mode 100644
index aabe2ff6..00000000
--- a/src/main/java/com/github/games647/fastlogin/PlayerData.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.github.games647.fastlogin;
-
-public class PlayerData {
-
- private final byte[] verifyToken;
- private final String username;
-
- public PlayerData(byte[] verifyToken, String username) {
- this.username = username;
- this.verifyToken = verifyToken;
- }
-
- public byte[] getVerifyToken() {
- return verifyToken;
- }
-
- public String getUsername() {
- return username;
- }
-}
diff --git a/src/main/java/com/github/games647/fastlogin/PlayerSession.java b/src/main/java/com/github/games647/fastlogin/PlayerSession.java
new file mode 100644
index 00000000..4b74f55e
--- /dev/null
+++ b/src/main/java/com/github/games647/fastlogin/PlayerSession.java
@@ -0,0 +1,56 @@
+package com.github.games647.fastlogin;
+
+/**
+ * Represents a client connecting to the server.
+ *
+ * This session is invalid if the player disconnects or the login was successful
+ */
+public class PlayerSession {
+
+ private final byte[] verifyToken;
+ private final String username;
+ private boolean verified;
+
+ public PlayerSession(byte[] verifyToken, String username) {
+ this.username = username;
+ this.verifyToken = verifyToken;
+ }
+
+ /**
+ * Gets the verify token the server sent to the client.
+ *
+ * @return the verify token from the server
+ */
+ public byte[] getVerifyToken() {
+ return verifyToken;
+ }
+
+ /**
+ * Gets the username the player sent to the server
+ *
+ * @return the client sent username
+ */
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Sets whether the player has a premium (paid account) account
+ * and valid session
+ *
+ * @param verified whether the player has valid session
+ */
+ public synchronized void setVerified(boolean verified) {
+ this.verified = verified;
+ }
+
+ /**
+ * Get whether the player has a premium (paid account) account
+ * and valid session
+ *
+ * @return whether the player has a valid session
+ */
+ public synchronized boolean isVerified() {
+ return verified;
+ }
+}
diff --git a/src/main/java/com/github/games647/fastlogin/hooks/AuthMeHook.java b/src/main/java/com/github/games647/fastlogin/hooks/AuthMeHook.java
new file mode 100644
index 00000000..33158df2
--- /dev/null
+++ b/src/main/java/com/github/games647/fastlogin/hooks/AuthMeHook.java
@@ -0,0 +1,25 @@
+package com.github.games647.fastlogin.hooks;
+
+import fr.xephi.authme.api.NewAPI;
+import fr.xephi.authme.cache.limbo.LimboCache;
+
+import org.bukkit.entity.Player;
+
+/**
+ * Github: https://github.com/Xephi/AuthMeReloaded/
+ * Project page: dev.bukkit.org/bukkit-plugins/authme-reloaded/
+ */
+public class AuthMeHook implements AuthPlugin {
+
+ @Override
+ public void forceLogin(Player player) {
+ //here is the gamemode, inventory ... saved
+ if (!LimboCache.getInstance().hasLimboPlayer(player.getName().toLowerCase())) {
+ //add cache entry - otherwise logging in wouldn't work
+ LimboCache.getInstance().addLimboPlayer(player);
+ }
+
+ //skips registration and login
+ NewAPI.getInstance().forceLogin(player);
+ }
+}
diff --git a/src/main/java/com/github/games647/fastlogin/hooks/AuthPlugin.java b/src/main/java/com/github/games647/fastlogin/hooks/AuthPlugin.java
new file mode 100644
index 00000000..73cf66d8
--- /dev/null
+++ b/src/main/java/com/github/games647/fastlogin/hooks/AuthPlugin.java
@@ -0,0 +1,16 @@
+package com.github.games647.fastlogin.hooks;
+
+import org.bukkit.entity.Player;
+
+/**
+ * Represents a supporting authentication plugin
+ */
+public interface AuthPlugin {
+
+ /**
+ * Login the premium (paid account) player
+ *
+ * @param player the player that needs to be logged in
+ */
+ void forceLogin(Player player);
+}
diff --git a/src/main/java/com/github/games647/fastlogin/hooks/CrazyLoginHook.java b/src/main/java/com/github/games647/fastlogin/hooks/CrazyLoginHook.java
new file mode 100644
index 00000000..c632c9ae
--- /dev/null
+++ b/src/main/java/com/github/games647/fastlogin/hooks/CrazyLoginHook.java
@@ -0,0 +1,31 @@
+package com.github.games647.fastlogin.hooks;
+
+import de.st_ddt.crazylogin.CrazyLogin;
+import de.st_ddt.crazylogin.data.LoginPlayerData;
+import de.st_ddt.crazylogin.databases.CrazyLoginDataDatabase;
+
+import org.bukkit.entity.Player;
+
+/**
+ * Github: https://github.com/ST-DDT/CrazyLogin
+ * Project page: http://dev.bukkit.org/server-mods/crazylogin/
+ */
+public class CrazyLoginHook implements AuthPlugin {
+
+ @Override
+ public void forceLogin(Player player) {
+ CrazyLogin crazyLoginPlugin = CrazyLogin.getPlugin();
+ CrazyLoginDataDatabase crazyDatabase = crazyLoginPlugin.getCrazyDatabase();
+
+ LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player.getName());
+ if (playerData == null) {
+ //create a fake account - this will be saved to the database with the password=FAILEDLOADING
+ //user cannot login with that password unless the admin uses plain text
+ playerData = new LoginPlayerData(player);
+ crazyDatabase.save(playerData);
+ } else {
+ //mark the account as logged in
+ playerData.setLoggedIn(true);
+ }
+ }
+}
diff --git a/src/main/java/com/github/games647/fastlogin/hooks/LoginSecurityHook.java b/src/main/java/com/github/games647/fastlogin/hooks/LoginSecurityHook.java
new file mode 100644
index 00000000..aea19697
--- /dev/null
+++ b/src/main/java/com/github/games647/fastlogin/hooks/LoginSecurityHook.java
@@ -0,0 +1,29 @@
+package com.github.games647.fastlogin.hooks;
+
+import com.lenis0012.bukkit.ls.LoginSecurity;
+
+import org.bukkit.entity.Player;
+
+/**
+ * Github: http://dev.bukkit.org/bukkit-plugins/loginsecurity/
+ * Project page: https://github.com/lenis0012/LoginSecurity-2
+ *
+ * on join: https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/LoginSecurity.java#L282
+ */
+public class LoginSecurityHook implements AuthPlugin {
+
+ @Override
+ public void forceLogin(Player player) {
+ //Login command of this plugin: (How the plugin logs the player in)
+ //https://github.com/lenis0012/LoginSecurity-2/blob/master/src/main/java/com/lenis0012/bukkit/ls/commands/LoginCommand.java#L39
+ LoginSecurity securityPlugin = LoginSecurity.instance;
+ String name = player.getName().toLowerCase();
+
+ //mark the user as logged in
+ securityPlugin.authList.remove(name);
+ //cancel timeout timer
+ securityPlugin.thread.timeout.remove(name);
+ //remove effects
+ securityPlugin.rehabPlayer(player, name);
+ }
+}
diff --git a/src/main/java/com/github/games647/fastlogin/hooks/xAuthHook.java b/src/main/java/com/github/games647/fastlogin/hooks/xAuthHook.java
new file mode 100644
index 00000000..8a297424
--- /dev/null
+++ b/src/main/java/com/github/games647/fastlogin/hooks/xAuthHook.java
@@ -0,0 +1,36 @@
+package com.github.games647.fastlogin.hooks;
+
+import de.luricos.bukkit.xAuth.xAuth;
+import de.luricos.bukkit.xAuth.xAuthPlayer;
+import de.luricos.bukkit.xAuth.xAuthPlayer.Status;
+
+import java.sql.Timestamp;
+
+import org.bukkit.entity.Player;
+
+/**
+ * Github: https://github.com/LycanDevelopment/xAuth/
+ * Project page: http://dev.bukkit.org/bukkit-plugins/xauth/
+ */
+public class xAuthHook implements AuthPlugin {
+
+ @Override
+ public void forceLogin(Player player) {
+ xAuth xAuthPlugin = xAuth.getPlugin();
+
+ xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
+ //we checked that the player is premium (paid account)
+ xAuthPlayer.setPremium(true);
+ //mark the player online
+ xAuthPlugin.getAuthClass(xAuthPlayer).online(xAuthPlayer.getName());
+
+ //update last login time
+ xAuthPlayer.setLoginTime(new Timestamp(System.currentTimeMillis()));
+
+ //mark the player as logged in
+ xAuthPlayer.setStatus(Status.AUTHENTICATED);
+
+ //restore inventory
+ xAuthPlugin.getPlayerManager().unprotect(xAuthPlayer);
+ }
+}
diff --git a/src/main/java/com/github/games647/fastlogin/listener/EncryptionPacketListener.java b/src/main/java/com/github/games647/fastlogin/listener/EncryptionPacketListener.java
index 75b43e60..8eaf70a8 100644
--- a/src/main/java/com/github/games647/fastlogin/listener/EncryptionPacketListener.java
+++ b/src/main/java/com/github/games647/fastlogin/listener/EncryptionPacketListener.java
@@ -7,15 +7,17 @@ import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.injector.server.SocketInjector;
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
+import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
+import com.github.games647.fastlogin.Encryption;
import com.github.games647.fastlogin.FastLogin;
-import com.github.games647.fastlogin.PlayerData;
+import com.github.games647.fastlogin.PlayerSession;
import java.io.BufferedReader;
-import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.security.PrivateKey;
@@ -24,29 +26,35 @@ import java.util.logging.Level;
import javax.crypto.SecretKey;
-import net.minecraft.server.v1_8_R3.MinecraftEncryption;
-import net.minecraft.server.v1_8_R3.NetworkManager;
-
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
+/**
+ * Receiving packet information:
+ * http://wiki.vg/Protocol#Encryption_Response
+ *
+ * sharedSecret=encrypted byte array
+ * verify token=encrypted byte array
+ */
public class EncryptionPacketListener extends PacketAdapter {
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
private final ProtocolManager protocolManager;
- private final FastLogin fastLogin;
+ //hides the inherit Plugin plugin field, but we need this type
+ private final FastLogin plugin;
public EncryptionPacketListener(FastLogin 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());
- this.fastLogin = plugin;
+ this.plugin = plugin;
this.protocolManager = protocolManger;
}
- /*
+ /**
* C->S : Handshake State=2
* C->S : Login Start
* S->C : Encryption Key Request
@@ -54,92 +62,138 @@ public class EncryptionPacketListener extends PacketAdapter {
* 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 event) {
- PacketContainer packet = event.getPacket();
- Player player = event.getPlayer();
+ public void onPacketReceiving(PacketEvent packetEvent) {
+ PacketContainer packet = packetEvent.getPacket();
+ Player player = packetEvent.getPlayer();
- final byte[] sharedSecret = packet.getByteArrays().read(0);
+ //the player name is unknown to ProtocolLib - now uses ip:port as key
+ String uniqueSessionKey = player.getAddress().toString();
+ PlayerSession session = plugin.getSessions().get(uniqueSessionKey);
+ if (session == null) {
+ disconnect(packetEvent, "Invalid request", Level.FINE
+ , "Player {0} tried to send encryption response"
+ + "on an invalid connection state"
+ , player.getAddress());
+ return;
+ }
+
+ byte[] sharedSecret = packet.getByteArrays().read(0);
byte[] clientVerify = packet.getByteArrays().read(1);
- PrivateKey privateKey = fastLogin.getKeyPair().getPrivate();
-
- String addressString = player.getAddress().toString();
- PlayerData cachedEntry = fastLogin.getSession().asMap().get(addressString);
- byte[] serverVerify = cachedEntry.getVerifyToken();
- if (!Arrays.equals(serverVerify, MinecraftEncryption.b(privateKey, clientVerify))) {
- player.kickPlayer("Invalid token");
- event.setCancelled(true);
+ PrivateKey privateKey = plugin.getKeyPair().getPrivate();
+ byte[] serverVerify = session.getVerifyToken();
+ //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L182
+ if (!Arrays.equals(serverVerify, Encryption.decryptData(privateKey, clientVerify))) {
+ //check if the verify token are equal to the server sent one
+ disconnect(packetEvent, "Invalid token", Level.FINE
+ , "Player {0} ({1}) tried to login with an invalid verify token. "
+ + "Server: {2} Client: {3}"
+ , session.getUsername(), player.getAddress(), serverVerify, clientVerify);
return;
}
- //encrypt all following packets
- NetworkManager networkManager = getNetworkManager(event);
- SecretKey loginKey = MinecraftEncryption.a(privateKey, sharedSecret);
- networkManager.a(loginKey);
- String serverId = (new BigInteger(MinecraftEncryption.a("", fastLogin.getKeyPair().getPublic(), loginKey)))
+ SecretKey loginKey = Encryption.decryptSharedKey(privateKey, sharedSecret);
+ try {
+ //get the NMS connection handle of this player
+ Object networkManager = getNetworkManager(player);
+
+ //try to detect the method by parameters
+ Method encryptConnectionMethod = FuzzyReflection.fromObject(networkManager)
+ .getMethodByParameters("a", SecretKey.class);
+
+ //encrypt/decrypt following packets
+ //the client expects this behaviour
+ encryptConnectionMethod.invoke(networkManager, loginKey);
+ } catch (ReflectiveOperationException ex) {
+ disconnect(packetEvent, "Error occurred", Level.SEVERE, "Couldn't enable encryption", ex);
+ return;
+ }
+
+ //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L193
+ //generate the server id based on client and server data
+ String serverId = (new BigInteger(Encryption.getServerIdHash("", plugin.getKeyPair().getPublic(), loginKey)))
.toString(16);
- String username = cachedEntry.getUsername();
- if (!hasJoinedServer(username, serverId)) {
+ String username = session.getUsername();
+ if (hasJoinedServer(username, serverId)) {
+ session.setVerified(true);
+
+ receiveFakeStartPacket(username, player);
+ } else {
//user tried to fake a authentification
- player.kickPlayer("Invalid session");
- event.setCancelled(true);
- return;
+ disconnect(packetEvent, "Invalid session", Level.FINE
+ , "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
+ , session.getUsername(), player.getAddress(), serverId);
}
- //fake a new login packet
- PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START, true);
- WrappedGameProfile fakeProfile = WrappedGameProfile.fromOfflinePlayer(Bukkit.getOfflinePlayer(username));
- startPacket.getGameProfiles().write(0, fakeProfile);
- try {
- protocolManager.recieveClientPacket(event.getPlayer(), startPacket, false);
- } catch (InvocationTargetException | IllegalAccessException ex) {
- plugin.getLogger().log(Level.WARNING, null, ex);
- }
-
- event.setCancelled(true);
+ packetEvent.setCancelled(true);
}
- private NetworkManager getNetworkManager(PacketEvent event) throws IllegalArgumentException {
- SocketInjector injector = TemporaryPlayerFactory.getInjectorFromPlayer(event.getPlayer());
- NetworkManager networkManager = null;
- try {
- Field declaredField = injector.getClass().getDeclaredField("injector");
- declaredField.setAccessible(true);
+ private void disconnect(PacketEvent packetEvent, String kickMessage, Level logLevel, String logMessage
+ , Object... arguments) {
+ plugin.getLogger().log(logLevel, logMessage, arguments);
+ packetEvent.getPlayer().kickPlayer(kickMessage);
+ //cancel the event in order to prevent the server receiving an invalid packet
+ packetEvent.setCancelled(true);
+ }
- Object rawInjector = declaredField.get(injector);
+ private Object getNetworkManager(Player player)
+ throws SecurityException, IllegalAccessException, NoSuchFieldException {
+ SocketInjector injector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
+ Field declaredField = injector.getClass().getDeclaredField("injector");
+ declaredField.setAccessible(true);
- declaredField = rawInjector.getClass().getDeclaredField("networkManager");
- declaredField.setAccessible(true);
- networkManager = (NetworkManager) declaredField.get(rawInjector);
- } catch (IllegalAccessException | NoSuchFieldException ex) {
- plugin.getLogger().log(Level.WARNING, null, ex);
- }
+ Object rawInjector = declaredField.get(injector);
- return networkManager;
+ declaredField = rawInjector.getClass().getDeclaredField("networkManager");
+ declaredField.setAccessible(true);
+ return declaredField.get(rawInjector);
}
private boolean hasJoinedServer(String username, String serverId) {
try {
String url = HAS_JOINED_URL + "username=" + username + "&serverId=" + serverId;
- HttpURLConnection conn = fastLogin.getConnection(url);
+ HttpURLConnection conn = plugin.getConnection(url);
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = reader.readLine();
if (!line.equals("null")) {
- JSONObject object = (JSONObject) JSONValue.parse(line);
+ //validate parsing
+ JSONObject object = (JSONObject) JSONValue.parseWithException(line);
String uuid = (String) object.get("id");
String name = (String) object.get("name");
return true;
}
- } catch (IOException ex) {
- plugin.getLogger().log(Level.WARNING, null, ex);
+ } catch (Exception ex) {
+ //catch not only ioexceptions also parse and NPE on unexpected json format
+ plugin.getLogger().log(Level.WARNING, "Failed to verify if session is valid", ex);
}
return false;
}
+
+ private void receiveFakeStartPacket(String username, Player player) {
+ //fake a new login packet
+ //see StartPacketListener for packet information
+ PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START, true);
+
+ WrappedGameProfile fakeProfile = WrappedGameProfile.fromOfflinePlayer(Bukkit.getOfflinePlayer(username));
+ startPacket.getGameProfiles().write(0, fakeProfile);
+ try {
+ protocolManager.recieveClientPacket(player, 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
+ player.kickPlayer("Error occurred");
+ }
+ }
}
diff --git a/src/main/java/com/github/games647/fastlogin/listener/PlayerListener.java b/src/main/java/com/github/games647/fastlogin/listener/PlayerListener.java
index c8e816a7..9d886846 100644
--- a/src/main/java/com/github/games647/fastlogin/listener/PlayerListener.java
+++ b/src/main/java/com/github/games647/fastlogin/listener/PlayerListener.java
@@ -1,16 +1,10 @@
package com.github.games647.fastlogin.listener;
import com.github.games647.fastlogin.FastLogin;
-import com.github.games647.fastlogin.PlayerData;
+import com.github.games647.fastlogin.PlayerSession;
+import com.github.games647.fastlogin.hooks.AuthPlugin;
-import de.luricos.bukkit.xAuth.xAuth;
-import de.luricos.bukkit.xAuth.xAuthPlayer;
-import de.luricos.bukkit.xAuth.xAuthPlayer.Status;
-
-import fr.xephi.authme.api.NewAPI;
-import fr.xephi.authme.cache.limbo.LimboCache;
-
-import java.sql.Timestamp;
+import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -21,9 +15,11 @@ import org.bukkit.event.player.PlayerJoinEvent;
public class PlayerListener implements Listener {
private final FastLogin plugin;
+ private final AuthPlugin authPlugin;
- public PlayerListener(FastLogin plugin) {
+ public PlayerListener(FastLogin plugin, AuthPlugin authPlugin) {
this.plugin = plugin;
+ this.authPlugin = authPlugin;
}
@EventHandler(ignoreCancelled = true)
@@ -31,32 +27,17 @@ public class PlayerListener implements Listener {
final Player player = joinEvent.getPlayer();
String address = player.getAddress().toString();
- PlayerData session = plugin.getSession().asMap().get(address);
- if (session != null && session.getUsername().equals(player.getName())) {
+ PlayerSession session = plugin.getSessions().get(address);
+ //check if it's the same player as we checked before
+ if (session != null && session.getUsername().equals(player.getName())
+ && session.isVerified()) {
Bukkit.getScheduler().runTaskLater(plugin, () -> {
- doLogin(player);
+ if (player.isOnline()) {
+ plugin.getLogger().log(Level.FINER, "Logging player {0} in", player.getName());
+ authPlugin.forceLogin(player);
+ }
+ //Wait before auth plugin initializes the player
}, 1 * 20L);
}
}
-
- private void doLogin(Player player) {
- if (Bukkit.getPluginManager().isPluginEnabled("AuthMe")) {
- //add cache entry - otherwise loggin wouldn't work
- LimboCache.getInstance().addLimboPlayer(player);
-
- //skips registration and login
- NewAPI.getInstance().forceLogin(player);
- } else if (Bukkit.getPluginManager().isPluginEnabled("xAuth")) {
- xAuth xAuthPlugin = xAuth.getPlugin();
-
- xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
- xAuthPlayer.setPremium(true);
- xAuthPlugin.getAuthClass(xAuthPlayer).online(xAuthPlayer.getName());
- xAuthPlayer.setLoginTime(new Timestamp(System.currentTimeMillis()));
-
- xAuthPlayer.setStatus(Status.AUTHENTICATED);
-
- xAuthPlugin.getPlayerManager().unprotect(xAuthPlayer);
- }
- }
}
diff --git a/src/main/java/com/github/games647/fastlogin/listener/StartPacketListener.java b/src/main/java/com/github/games647/fastlogin/listener/StartPacketListener.java
index b0f3dfb5..680f920b 100644
--- a/src/main/java/com/github/games647/fastlogin/listener/StartPacketListener.java
+++ b/src/main/java/com/github/games647/fastlogin/listener/StartPacketListener.java
@@ -6,7 +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.FastLogin;
-import com.github.games647.fastlogin.PlayerData;
+import com.github.games647.fastlogin.PlayerSession;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
@@ -15,27 +15,40 @@ import java.security.PublicKey;
import java.util.Random;
import java.util.logging.Level;
+import java.util.regex.Pattern;
import org.bukkit.entity.Player;
+/**
+ * Receiving packet information:
+ * http://wiki.vg/Protocol#Login_Start
+ *
+ * String=Username
+ */
public class StartPacketListener extends PacketAdapter {
- //only premium members have a uuid from there
+ //only premium (paid account) users have a uuid from there
private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
+ private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
private final ProtocolManager protocolManager;
- private final FastLogin fastLogin;
+ //hides the inherit Plugin plugin field, but we need a more detailed type than just Plugin
+ private final FastLogin plugin;
+ //just create a new once on plugin enable
private final Random random = new Random();
+ //compile the pattern on plugin enable
+ private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
public StartPacketListener(FastLogin 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.START).optionAsync());
- this.fastLogin = plugin;
+ this.plugin = plugin;
this.protocolManager = protocolManger;
}
- /*
+ /**
* C->S : Handshake State=2
* C->S : Login Start
* S->C : Encryption Key Request
@@ -43,48 +56,72 @@ public class StartPacketListener extends PacketAdapter {
* 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) {
PacketContainer packet = packetEvent.getPacket();
Player player = packetEvent.getPlayer();
+ //this includes ip and port. Should be unique for 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);
+
String username = packet.getGameProfiles().read(0).getName();
+ plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting to the server"
+ , new Object[]{sessionKey, username});
+ //do premium login process
if (isPremium(username)) {
- //do premium login process
- try {
- PacketContainer newPacket = protocolManager.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN, true);
-
- //constr ServerID=""
- //public key=plugin.getPublic
- newPacket.getSpecificModifier(PublicKey.class).write(0, fastLogin.getKeyPair().getPublic());
- byte[] verifyToken = new byte[4];
- random.nextBytes(verifyToken);
- newPacket.getByteArrays().write(0, verifyToken);
-
- String addressString = player.getAddress().toString();
- fastLogin.getSession().asMap().put(addressString, new PlayerData(verifyToken, username));
-
- protocolManager.sendServerPacket(player, newPacket, false);
- } catch (InvocationTargetException ex) {
- plugin.getLogger().log(Level.SEVERE, null, ex);
- }
-
- //cancel only if the player is premium
- packetEvent.setCancelled(true);
+ //minecraft server implementation
+ //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
+ sentEncryptionRequest(sessionKey, username, player, packetEvent);
}
}
private boolean isPremium(String playerName) {
- try {
- final HttpURLConnection connection = fastLogin.getConnection(UUID_LINK + playerName);
- final int responseCode = connection.getResponseCode();
+ if (playernameMatcher.matcher(playerName).matches()) {
+ //only make a API call if the name is valid existing mojang account
+ try {
+ HttpURLConnection connection = plugin.getConnection(UUID_LINK + playerName);
+ int responseCode = connection.getResponseCode();
- return responseCode == HttpURLConnection.HTTP_OK;
- } catch (IOException ex) {
- plugin.getLogger().log(Level.SEVERE, null, ex);
+ return responseCode == HttpURLConnection.HTTP_OK;
+ //204 - no content for not found
+ } catch (IOException ex) {
+ plugin.getLogger().log(Level.SEVERE, "Failed to check if player has a paid account", ex);
+ }
}
return false;
}
+
+ private void sentEncryptionRequest(String sessionKey, String username, Player player, PacketEvent packetEvent) {
+ 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, true);
+
+ newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getKeyPair().getPublic());
+ byte[] verifyToken = new byte[4];
+ random.nextBytes(verifyToken);
+ newPacket.getByteArrays().write(0, verifyToken);
+
+ protocolManager.sendServerPacket(player, newPacket, false);
+
+ //cancel only if the player has a paid account otherwise login as normal offline player
+ packetEvent.setCancelled(true);
+ plugin.getSessions().put(sessionKey, new PlayerSession(verifyToken, username));
+ } catch (InvocationTargetException ex) {
+ plugin.getLogger().log(Level.SEVERE, "Cannot send encryption packet. Falling back to normal login", ex);
+ }
+ }
}
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 6a5457db..332912ed 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -11,4 +11,9 @@ description: |
website: ${project.url}
dev-url: ${project.url}
-depend: [ProtocolLib]
\ No newline at end of file
+depend: [ProtocolLib]
+softdepend:
+ - xAuth
+ - AuthMe
+ - CrazyLogin
+ - LoginSecurity