mirror of
https://github.com/TuxCoding/FastLogin.git
synced 2025-07-30 02:37:34 +02:00
Added support for CrazyLogin and LoginSecurity + Code cleanup + Added a lot of comments + Version independent
This commit is contained in:
@ -9,3 +9,5 @@ Requirements:
|
|||||||
* An auth plugin. Supported Plugins:
|
* An auth plugin. Supported Plugins:
|
||||||
* [AuthMe](http://dev.bukkit.org/bukkit-plugins/authme-reloaded/)
|
* [AuthMe](http://dev.bukkit.org/bukkit-plugins/authme-reloaded/)
|
||||||
* [xAuth](http://dev.bukkit.org/bukkit-plugins/xauth/)
|
* [xAuth](http://dev.bukkit.org/bukkit-plugins/xauth/)
|
||||||
|
* [CrazyLogin](http://dev.bukkit.org/bukkit-plugins/crazylogin/)
|
||||||
|
* [LoginSecurity](http://dev.bukkit.org/bukkit-plugins/loginsecurity/)
|
||||||
|
BIN
lib/CrazyCore v10.7.7.jar
Normal file
BIN
lib/CrazyCore v10.7.7.jar
Normal file
Binary file not shown.
BIN
lib/CrazyLogin v7.23.2.jar
Normal file
BIN
lib/CrazyLogin v7.23.2.jar
Normal file
Binary file not shown.
BIN
lib/LoginSecurity v2.0.10.jar
Normal file
BIN
lib/LoginSecurity v2.0.10.jar
Normal file
Binary file not shown.
34
pom.xml
34
pom.xml
@ -8,11 +8,11 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>FastLogin</name>
|
<name>FastLogin</name>
|
||||||
<version>0.1</version>
|
<version>0.2</version>
|
||||||
<inceptionYear>2015</inceptionYear>
|
<inceptionYear>2015</inceptionYear>
|
||||||
<url>http://dev.bukkit.org/bukkit-plugins/fastlogin</url>
|
<url>http://dev.bukkit.org/bukkit-plugins/fastlogin</url>
|
||||||
<description>
|
<description>
|
||||||
Automatically logins premium player on a offline mode server
|
Automatically logins premium (paid accounts) player on a offline mode server
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
@ -43,7 +43,6 @@
|
|||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.2</version>
|
<version>3.2</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<!--So many people still use Java 6 ;( http://mcstats.org/global/#Java+Version-->
|
|
||||||
<source>1.8</source>
|
<source>1.8</source>
|
||||||
<target>1.8</target>
|
<target>1.8</target>
|
||||||
<showWarnings>true</showWarnings>
|
<showWarnings>true</showWarnings>
|
||||||
@ -110,7 +109,7 @@
|
|||||||
<!--Server API-->
|
<!--Server API-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.spigotmc</groupId>
|
<groupId>org.spigotmc</groupId>
|
||||||
<artifactId>spigot</artifactId>
|
<artifactId>spigot-api</artifactId>
|
||||||
<version>1.8.8-R0.1-SNAPSHOT</version>
|
<version>1.8.8-R0.1-SNAPSHOT</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
@ -119,7 +118,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.comphenix.protocol</groupId>
|
<groupId>com.comphenix.protocol</groupId>
|
||||||
<artifactId>ProtocolLib</artifactId>
|
<artifactId>ProtocolLib</artifactId>
|
||||||
<version>3.6.3-SNAPSHOT</version>
|
<version>3.6.5-SNAPSHOT</version>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@ -134,6 +133,7 @@
|
|||||||
<groupId>de.luricos.bukkit</groupId>
|
<groupId>de.luricos.bukkit</groupId>
|
||||||
<artifactId>xAuth</artifactId>
|
<artifactId>xAuth</artifactId>
|
||||||
<version>2.6</version>
|
<version>2.6</version>
|
||||||
|
<!--These artifacts produce conflicts on downloading-->
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<groupId>net.gravitydevelopment.updater</groupId>
|
<groupId>net.gravitydevelopment.updater</groupId>
|
||||||
@ -145,5 +145,29 @@
|
|||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>de.st_ddt.crazy</groupId>
|
||||||
|
<artifactId>CrazyCore</artifactId>
|
||||||
|
<version>10.7.7</version>
|
||||||
|
<scope>system</scope>
|
||||||
|
<systemPath>${project.basedir}/lib/CrazyCore v10.7.7.jar</systemPath>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>de.st_ddt.crazy</groupId>
|
||||||
|
<artifactId>CrazyLogin</artifactId>
|
||||||
|
<version>7.23</version>
|
||||||
|
<scope>system</scope>
|
||||||
|
<systemPath>${project.basedir}/lib/CrazyLogin v7.23.2.jar</systemPath>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>me.lenis0012.ls</groupId>
|
||||||
|
<artifactId>LoginSecurity</artifactId>
|
||||||
|
<version>2.0.10</version>
|
||||||
|
<scope>system</scope>
|
||||||
|
<systemPath>${project.basedir}/lib/LoginSecurity v2.0.10.jar</systemPath>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
135
src/main/java/com/github/games647/fastlogin/Encryption.java
Normal file
135
src/main/java/com/github/games647/fastlogin/Encryption.java
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -3,91 +3,144 @@ package com.github.games647.fastlogin;
|
|||||||
import com.github.games647.fastlogin.listener.PlayerListener;
|
import com.github.games647.fastlogin.listener.PlayerListener;
|
||||||
import com.comphenix.protocol.ProtocolLibrary;
|
import com.comphenix.protocol.ProtocolLibrary;
|
||||||
import com.comphenix.protocol.ProtocolManager;
|
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.EncryptionPacketListener;
|
||||||
import com.github.games647.fastlogin.listener.StartPacketListener;
|
import com.github.games647.fastlogin.listener.StartPacketListener;
|
||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.CacheLoader;
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.reflect.ClassPath;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
public class FastLogin extends JavaPlugin {
|
public class FastLogin extends JavaPlugin {
|
||||||
|
|
||||||
private final KeyPair keyPair = generateKey();
|
private static final int TIMEOUT = 15000;
|
||||||
private final Cache<String, PlayerData> session = CacheBuilder.newBuilder()
|
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<String, PlayerSession> session = SafeCacheBuilder.<String, PlayerSession>newBuilder()
|
||||||
|
//mapped by ip:port
|
||||||
.expireAfterWrite(2, TimeUnit.MINUTES)
|
.expireAfterWrite(2, TimeUnit.MINUTES)
|
||||||
.build();
|
//2 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
|
||||||
|
.build(new CacheLoader<String, PlayerSession>() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public PlayerSession load(String key) throws Exception {
|
||||||
if (!isEnabled()) {
|
//A key should be inserted manually on start packet
|
||||||
return;
|
throw new UnsupportedOperationException("Not supported");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
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
|
@Override
|
||||||
public void onLoad() {
|
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()) {
|
if (getServer().getOnlineMode()) {
|
||||||
getLogger().severe("Server have to be in offline mode");
|
getLogger().severe("Server have to be in offline mode");
|
||||||
|
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateKey();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private KeyPair generateKey() {
|
@Override
|
||||||
try {
|
public void onEnable() {
|
||||||
KeyPairGenerator keypairgenerator = KeyPairGenerator.getInstance("RSA");
|
if (!isEnabled() || !registerHooks()) {
|
||||||
|
return;
|
||||||
keypairgenerator.initialize(1024);
|
|
||||||
return keypairgenerator.generateKeyPair();
|
|
||||||
} catch (NoSuchAlgorithmException noSuchAlgorithmException) {
|
|
||||||
//Should be default existing in every vm
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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<String, PlayerData> 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<String, PlayerSession> getSessions() {
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the server KeyPair
|
||||||
|
*
|
||||||
|
* @return the server KeyPair
|
||||||
|
*/
|
||||||
public KeyPair getKeyPair() {
|
public KeyPair getKeyPair() {
|
||||||
return keyPair;
|
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 {
|
public HttpURLConnection getConnection(String url) throws IOException {
|
||||||
final HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||||
connection.setConnectTimeout(15000);
|
connection.setConnectTimeout(TIMEOUT);
|
||||||
connection.setReadTimeout(15000);
|
connection.setReadTimeout(TIMEOUT);
|
||||||
|
//the new Mojang API just uses json as response
|
||||||
connection.setRequestProperty("Content-Type", "application/json");
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
connection.setRequestProperty("User-Agent", "Premium-Checker");
|
connection.setRequestProperty("User-Agent", USER_AGENT);
|
||||||
|
|
||||||
return connection;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -7,15 +7,17 @@ import com.comphenix.protocol.events.PacketContainer;
|
|||||||
import com.comphenix.protocol.events.PacketEvent;
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
import com.comphenix.protocol.injector.server.SocketInjector;
|
import com.comphenix.protocol.injector.server.SocketInjector;
|
||||||
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
|
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
|
||||||
|
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||||
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
||||||
|
import com.github.games647.fastlogin.Encryption;
|
||||||
import com.github.games647.fastlogin.FastLogin;
|
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.BufferedReader;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
@ -24,29 +26,35 @@ import java.util.logging.Level;
|
|||||||
|
|
||||||
import javax.crypto.SecretKey;
|
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.Bukkit;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.json.simple.JSONObject;
|
import org.json.simple.JSONObject;
|
||||||
import org.json.simple.JSONValue;
|
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 {
|
public class EncryptionPacketListener extends PacketAdapter {
|
||||||
|
|
||||||
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
|
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
|
||||||
|
|
||||||
private final ProtocolManager protocolManager;
|
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) {
|
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());
|
super(params(plugin, PacketType.Login.Client.ENCRYPTION_BEGIN).optionAsync());
|
||||||
|
|
||||||
this.fastLogin = plugin;
|
this.plugin = plugin;
|
||||||
this.protocolManager = protocolManger;
|
this.protocolManager = protocolManger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* C->S : Handshake State=2
|
* C->S : Handshake State=2
|
||||||
* C->S : Login Start
|
* C->S : Login Start
|
||||||
* S->C : Encryption Key Request
|
* S->C : Encryption Key Request
|
||||||
@ -54,92 +62,138 @@ public class EncryptionPacketListener extends PacketAdapter {
|
|||||||
* C->S : Encryption Key Response
|
* C->S : Encryption Key Response
|
||||||
* (Server Auth, Both enable encryption)
|
* (Server Auth, Both enable encryption)
|
||||||
* S->C : Login Success (*)
|
* 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
|
@Override
|
||||||
public void onPacketReceiving(PacketEvent event) {
|
public void onPacketReceiving(PacketEvent packetEvent) {
|
||||||
PacketContainer packet = event.getPacket();
|
PacketContainer packet = packetEvent.getPacket();
|
||||||
Player player = event.getPlayer();
|
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);
|
byte[] clientVerify = packet.getByteArrays().read(1);
|
||||||
|
|
||||||
PrivateKey privateKey = fastLogin.getKeyPair().getPrivate();
|
PrivateKey privateKey = plugin.getKeyPair().getPrivate();
|
||||||
|
byte[] serverVerify = session.getVerifyToken();
|
||||||
String addressString = player.getAddress().toString();
|
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L182
|
||||||
PlayerData cachedEntry = fastLogin.getSession().asMap().get(addressString);
|
if (!Arrays.equals(serverVerify, Encryption.decryptData(privateKey, clientVerify))) {
|
||||||
byte[] serverVerify = cachedEntry.getVerifyToken();
|
//check if the verify token are equal to the server sent one
|
||||||
if (!Arrays.equals(serverVerify, MinecraftEncryption.b(privateKey, clientVerify))) {
|
disconnect(packetEvent, "Invalid token", Level.FINE
|
||||||
player.kickPlayer("Invalid token");
|
, "Player {0} ({1}) tried to login with an invalid verify token. "
|
||||||
event.setCancelled(true);
|
+ "Server: {2} Client: {3}"
|
||||||
|
, session.getUsername(), player.getAddress(), serverVerify, clientVerify);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//encrypt all following packets
|
SecretKey loginKey = Encryption.decryptSharedKey(privateKey, sharedSecret);
|
||||||
NetworkManager networkManager = getNetworkManager(event);
|
try {
|
||||||
SecretKey loginKey = MinecraftEncryption.a(privateKey, sharedSecret);
|
//get the NMS connection handle of this player
|
||||||
networkManager.a(loginKey);
|
Object networkManager = getNetworkManager(player);
|
||||||
String serverId = (new BigInteger(MinecraftEncryption.a("", fastLogin.getKeyPair().getPublic(), loginKey)))
|
|
||||||
|
//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);
|
.toString(16);
|
||||||
|
|
||||||
String username = cachedEntry.getUsername();
|
String username = session.getUsername();
|
||||||
if (!hasJoinedServer(username, serverId)) {
|
if (hasJoinedServer(username, serverId)) {
|
||||||
|
session.setVerified(true);
|
||||||
|
|
||||||
|
receiveFakeStartPacket(username, player);
|
||||||
|
} else {
|
||||||
//user tried to fake a authentification
|
//user tried to fake a authentification
|
||||||
player.kickPlayer("Invalid session");
|
disconnect(packetEvent, "Invalid session", Level.FINE
|
||||||
event.setCancelled(true);
|
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
|
||||||
return;
|
, session.getUsername(), player.getAddress(), serverId);
|
||||||
}
|
}
|
||||||
|
|
||||||
//fake a new login packet
|
packetEvent.setCancelled(true);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private NetworkManager getNetworkManager(PacketEvent event) throws IllegalArgumentException {
|
private void disconnect(PacketEvent packetEvent, String kickMessage, Level logLevel, String logMessage
|
||||||
SocketInjector injector = TemporaryPlayerFactory.getInjectorFromPlayer(event.getPlayer());
|
, Object... arguments) {
|
||||||
NetworkManager networkManager = null;
|
plugin.getLogger().log(logLevel, logMessage, arguments);
|
||||||
try {
|
packetEvent.getPlayer().kickPlayer(kickMessage);
|
||||||
Field declaredField = injector.getClass().getDeclaredField("injector");
|
//cancel the event in order to prevent the server receiving an invalid packet
|
||||||
declaredField.setAccessible(true);
|
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");
|
Object rawInjector = declaredField.get(injector);
|
||||||
declaredField.setAccessible(true);
|
|
||||||
networkManager = (NetworkManager) declaredField.get(rawInjector);
|
|
||||||
} catch (IllegalAccessException | NoSuchFieldException ex) {
|
|
||||||
plugin.getLogger().log(Level.WARNING, null, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return networkManager;
|
declaredField = rawInjector.getClass().getDeclaredField("networkManager");
|
||||||
|
declaredField.setAccessible(true);
|
||||||
|
return declaredField.get(rawInjector);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasJoinedServer(String username, String serverId) {
|
private boolean hasJoinedServer(String username, String serverId) {
|
||||||
try {
|
try {
|
||||||
String url = HAS_JOINED_URL + "username=" + username + "&serverId=" + serverId;
|
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()));
|
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||||
String line = reader.readLine();
|
String line = reader.readLine();
|
||||||
if (!line.equals("null")) {
|
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 uuid = (String) object.get("id");
|
||||||
String name = (String) object.get("name");
|
String name = (String) object.get("name");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (IOException ex) {
|
} catch (Exception ex) {
|
||||||
plugin.getLogger().log(Level.WARNING, null, 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;
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,10 @@
|
|||||||
package com.github.games647.fastlogin.listener;
|
package com.github.games647.fastlogin.listener;
|
||||||
|
|
||||||
import com.github.games647.fastlogin.FastLogin;
|
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 java.util.logging.Level;
|
||||||
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 org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
@ -21,9 +15,11 @@ import org.bukkit.event.player.PlayerJoinEvent;
|
|||||||
public class PlayerListener implements Listener {
|
public class PlayerListener implements Listener {
|
||||||
|
|
||||||
private final FastLogin plugin;
|
private final FastLogin plugin;
|
||||||
|
private final AuthPlugin authPlugin;
|
||||||
|
|
||||||
public PlayerListener(FastLogin plugin) {
|
public PlayerListener(FastLogin plugin, AuthPlugin authPlugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
|
this.authPlugin = authPlugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(ignoreCancelled = true)
|
@EventHandler(ignoreCancelled = true)
|
||||||
@ -31,32 +27,17 @@ public class PlayerListener implements Listener {
|
|||||||
final Player player = joinEvent.getPlayer();
|
final Player player = joinEvent.getPlayer();
|
||||||
String address = player.getAddress().toString();
|
String address = player.getAddress().toString();
|
||||||
|
|
||||||
PlayerData session = plugin.getSession().asMap().get(address);
|
PlayerSession session = plugin.getSessions().get(address);
|
||||||
if (session != null && session.getUsername().equals(player.getName())) {
|
//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, () -> {
|
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);
|
}, 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import com.comphenix.protocol.events.PacketAdapter;
|
|||||||
import com.comphenix.protocol.events.PacketContainer;
|
import com.comphenix.protocol.events.PacketContainer;
|
||||||
import com.comphenix.protocol.events.PacketEvent;
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
import com.github.games647.fastlogin.FastLogin;
|
import com.github.games647.fastlogin.FastLogin;
|
||||||
import com.github.games647.fastlogin.PlayerData;
|
import com.github.games647.fastlogin.PlayerSession;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
@ -15,27 +15,40 @@ import java.security.PublicKey;
|
|||||||
|
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receiving packet information:
|
||||||
|
* http://wiki.vg/Protocol#Login_Start
|
||||||
|
*
|
||||||
|
* String=Username
|
||||||
|
*/
|
||||||
public class StartPacketListener extends PacketAdapter {
|
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 UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
|
||||||
|
private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
|
||||||
|
|
||||||
private final ProtocolManager protocolManager;
|
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();
|
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) {
|
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());
|
super(params(plugin, PacketType.Login.Client.START).optionAsync());
|
||||||
|
|
||||||
this.fastLogin = plugin;
|
this.plugin = plugin;
|
||||||
this.protocolManager = protocolManger;
|
this.protocolManager = protocolManger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* C->S : Handshake State=2
|
* C->S : Handshake State=2
|
||||||
* C->S : Login Start
|
* C->S : Login Start
|
||||||
* S->C : Encryption Key Request
|
* S->C : Encryption Key Request
|
||||||
@ -43,48 +56,72 @@ public class StartPacketListener extends PacketAdapter {
|
|||||||
* C->S : Encryption Key Response
|
* C->S : Encryption Key Response
|
||||||
* (Server Auth, Both enable encryption)
|
* (Server Auth, Both enable encryption)
|
||||||
* S->C : Login Success (*)
|
* S->C : Login Success (*)
|
||||||
|
*
|
||||||
|
* On offline logins is Login Start followed by Login Success
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onPacketReceiving(PacketEvent packetEvent) {
|
public void onPacketReceiving(PacketEvent packetEvent) {
|
||||||
PacketContainer packet = packetEvent.getPacket();
|
PacketContainer packet = packetEvent.getPacket();
|
||||||
Player player = packetEvent.getPlayer();
|
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();
|
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)) {
|
if (isPremium(username)) {
|
||||||
//do premium login process
|
//minecraft server implementation
|
||||||
try {
|
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
|
||||||
PacketContainer newPacket = protocolManager.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN, true);
|
sentEncryptionRequest(sessionKey, username, player, packetEvent);
|
||||||
|
|
||||||
//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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPremium(String playerName) {
|
private boolean isPremium(String playerName) {
|
||||||
try {
|
if (playernameMatcher.matcher(playerName).matches()) {
|
||||||
final HttpURLConnection connection = fastLogin.getConnection(UUID_LINK + playerName);
|
//only make a API call if the name is valid existing mojang account
|
||||||
final int responseCode = connection.getResponseCode();
|
try {
|
||||||
|
HttpURLConnection connection = plugin.getConnection(UUID_LINK + playerName);
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
|
||||||
return responseCode == HttpURLConnection.HTTP_OK;
|
return responseCode == HttpURLConnection.HTTP_OK;
|
||||||
} catch (IOException ex) {
|
//204 - no content for not found
|
||||||
plugin.getLogger().log(Level.SEVERE, null, ex);
|
} catch (IOException ex) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "Failed to check if player has a paid account", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,4 +11,9 @@ description: |
|
|||||||
website: ${project.url}
|
website: ${project.url}
|
||||||
dev-url: ${project.url}
|
dev-url: ${project.url}
|
||||||
|
|
||||||
depend: [ProtocolLib]
|
depend: [ProtocolLib]
|
||||||
|
softdepend:
|
||||||
|
- xAuth
|
||||||
|
- AuthMe
|
||||||
|
- CrazyLogin
|
||||||
|
- LoginSecurity
|
||||||
|
Reference in New Issue
Block a user