forked from TuxCoding/FastLogin
Compare commits
9 Commits
add-postgr
...
0.2.3
Author | SHA1 | Date | |
---|---|---|---|
253181d34e | |||
8abe9562e0 | |||
987518e0f4 | |||
d573b11d77 | |||
7d9ffa407a | |||
e51adf6528 | |||
f0425a9046 | |||
7e97507334 | |||
4eeb6c64a3 |
50
.gitignore
vendored
50
.gitignore
vendored
@ -1,8 +1,42 @@
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
# Eclipse stuff
|
||||
/.classpath
|
||||
/.project
|
||||
/.settings
|
||||
|
||||
# netbeans
|
||||
/nbproject
|
||||
nb-configuration.xml
|
||||
|
||||
# maven
|
||||
/target
|
||||
|
||||
# vim
|
||||
.*.sw[a-p]
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
# various other potential build files
|
||||
/build
|
||||
/bin
|
||||
/dist
|
||||
/manifest.mf
|
||||
*.log
|
||||
|
||||
# Mac filesystem dust
|
||||
.DS_Store
|
||||
|
||||
# intellij
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
21
README.md
21
README.md
@ -1,2 +1,21 @@
|
||||
# FastLogin
|
||||
Checks if a minecraft player has a valid paid account. If so, they can skip offline authentification.
|
||||
|
||||
Checks if a minecraft player has a valid premium (paid account). If so, they can skip offline authentication.
|
||||
|
||||
###Commands:
|
||||
* /premium Marks the invoker as paid account
|
||||
* /premium [playername] Mark player specified as a paid account
|
||||
|
||||
###Premissions:
|
||||
* fastlogin.command.premium
|
||||
* fastlogin.command.premium.others
|
||||
|
||||
###Requirements:
|
||||
* [ProtocolLib](http://www.spigotmc.org/resources/protocollib.1997/)
|
||||
* Tested Bukkit 1.8.8 (could also work with other versions)
|
||||
* Java 7 or above
|
||||
* 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/)
|
||||
|
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.
180
pom.xml
Normal file
180
pom.xml
Normal file
@ -0,0 +1,180 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.github.games647</groupId>
|
||||
<!--This have to be in lowercase because it's used by plugin.yml-->
|
||||
<artifactId>fastlogin</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>FastLogin</name>
|
||||
<version>0.2.3</version>
|
||||
<inceptionYear>2015</inceptionYear>
|
||||
<url>https://github.com/games647/FastLogin</url>
|
||||
<description>
|
||||
Automatically logins premium (paid accounts) player on a offline mode server
|
||||
</description>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<!--Possibility to deploy directly to the plugins folder-->
|
||||
<outputDir>${basedir}/target</outputDir>
|
||||
</properties>
|
||||
|
||||
<issueManagement>
|
||||
<system>GitHub</system>
|
||||
<url>https://github.com/games647/FastLogin/issues</url>
|
||||
</issueManagement>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/games647/FastLogin</url>
|
||||
<connection>scm:git:git://github.com/games647/FastLogin.git</connection>
|
||||
<developerConnection>scm:git:ssh://git@github.com:games647/FastLogin.git</developerConnection>
|
||||
</scm>
|
||||
|
||||
<build>
|
||||
<defaultGoal>install</defaultGoal>
|
||||
<!--Just use the project name to replace an old version of the plugin if the user does only copy-paste-->
|
||||
<finalName>${project.name}</finalName>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.2</version>
|
||||
<configuration>
|
||||
<source>1.7</source>
|
||||
<target>1.7</target>
|
||||
<showWarnings>true</showWarnings>
|
||||
<showDeprecation>true</showDeprecation>
|
||||
<!--false means actual true http://jira.codehaus.org/browse/MCOMPILER-209-->
|
||||
<useIncrementalCompilation>false</useIncrementalCompilation>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>2.6</version>
|
||||
<configuration>
|
||||
<outputDirectory>${outputDir}</outputDirectory>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<!--Replace variables-->
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
|
||||
<!--Add the license to jar in order to see it in the final jar-->
|
||||
<resource>
|
||||
<directory>${basedir}</directory>
|
||||
<includes>
|
||||
<include>LICENSE</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<!--Bukkit-Server-API -->
|
||||
<repository>
|
||||
<id>spigot-repo</id>
|
||||
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||
</repository>
|
||||
|
||||
<!--ProtocolLib-->
|
||||
<repository>
|
||||
<id>dmulloy2-repo</id>
|
||||
<url>http://repo.dmulloy2.net/content/groups/public/</url>
|
||||
</repository>
|
||||
|
||||
<!--Authme Reloaded-->
|
||||
<repository>
|
||||
<id>xephi-repo</id>
|
||||
<url>http://ci.xephi.fr/plugin/repository/everything/</url>
|
||||
</repository>
|
||||
|
||||
<!--xAuth-->
|
||||
<repository>
|
||||
<id>luricos.de-repo</id>
|
||||
<url>http://repo.luricos.de/bukkit-plugins/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<!--Server API-->
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>1.8.8-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!--Library for listening and sending Minecraft packets-->
|
||||
<dependency>
|
||||
<groupId>com.comphenix.protocol</groupId>
|
||||
<artifactId>ProtocolLib</artifactId>
|
||||
<version>3.6.5-SNAPSHOT</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!--Login Plugins-->
|
||||
<dependency>
|
||||
<groupId>fr.xephi</groupId>
|
||||
<artifactId>authme</artifactId>
|
||||
<version>5.0-SNAPSHOT</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.luricos.bukkit</groupId>
|
||||
<artifactId>xAuth</artifactId>
|
||||
<version>2.6</version>
|
||||
<optional>true</optional>
|
||||
<!--These artifacts produce conflicts on downloading-->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>net.gravitydevelopment.updater</groupId>
|
||||
<artifactId>updater</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>net.ess3</groupId>
|
||||
<artifactId>EssentialsGroupManager</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!--No maven repository :(-->
|
||||
<dependency>
|
||||
<groupId>de.st_ddt.crazy</groupId>
|
||||
<artifactId>CrazyCore</artifactId>
|
||||
<version>10.7.7</version>
|
||||
<optional>true</optional>
|
||||
<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>
|
||||
<optional>true</optional>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/CrazyLogin v7.23.2.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<!--Maven repo down :(-->
|
||||
<dependency>
|
||||
<groupId>me.lenis0012.ls</groupId>
|
||||
<artifactId>LoginSecurity</artifactId>
|
||||
<version>2.0.10</version>
|
||||
<optional>true</optional>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/LoginSecurity v2.0.10.jar</systemPath>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
128
src/main/java/com/github/games647/fastlogin/Encryption.java
Normal file
128
src/main/java/com/github/games647/fastlogin/Encryption.java
Normal file
@ -0,0 +1,128 @@
|
||||
package com.github.games647.fastlogin;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Encryption and decryption minecraft util for connection between servers
|
||||
* and paid minecraft account clients.
|
||||
*
|
||||
* 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) {
|
||||
return digestOperation("SHA-1"
|
||||
, new byte[][]{serverId.getBytes(Charsets.ISO_8859_1), secretkey.getEncoded(), publickey.getEncoded()});
|
||||
}
|
||||
|
||||
private static byte[] digestOperation(String algo, byte[]... content) {
|
||||
try {
|
||||
MessageDigest messagedigest = MessageDigest.getInstance(algo);
|
||||
for (byte[] data : content) {
|
||||
messagedigest.update(data);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
172
src/main/java/com/github/games647/fastlogin/FastLogin.java
Normal file
172
src/main/java/com/github/games647/fastlogin/FastLogin.java
Normal file
@ -0,0 +1,172 @@
|
||||
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.CacheLoader;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.reflect.ClassPath;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
/**
|
||||
* This plugin checks if a player has a paid account and if so
|
||||
* tries to skip offline mode authentication.
|
||||
*/
|
||||
public class FastLogin extends JavaPlugin {
|
||||
|
||||
//http connection, read timeout and user agent for a connection to mojang api servers
|
||||
private static final int TIMEOUT = 10 * 1000;
|
||||
private static final String USER_AGENT = "Premium-Checker";
|
||||
|
||||
//provide a immutable key pair to be thread safe | used for encrypting and decrypting traffic
|
||||
private final KeyPair keyPair = Encryption.generateKeyPair();
|
||||
|
||||
//we need a thread-safe set because we access it async in the packet listener
|
||||
private final Set<String> enabledPremium = Sets.newConcurrentHashSet();
|
||||
|
||||
//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()
|
||||
//2 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
|
||||
.expireAfterWrite(2, TimeUnit.MINUTES)
|
||||
//mapped by ip:port -> PlayerSession
|
||||
.build(new CacheLoader<String, PlayerSession>() {
|
||||
|
||||
@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 after a restart so check it here
|
||||
if (getServer().getOnlineMode()) {
|
||||
//we need to require offline to prevent a session request for a offline player
|
||||
getLogger().severe("Server have to be in offline mode");
|
||||
|
||||
setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (!isEnabled() || !registerHooks()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//register packet listeners on success
|
||||
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
|
||||
protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager));
|
||||
protocolManager.addPacketListener(new StartPacketListener(this, protocolManager));
|
||||
|
||||
//register commands using a unique name
|
||||
getCommand("premium").setExecutor(new PremiumCommand(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
//clean up
|
||||
session.clear();
|
||||
enabledPremium.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the server KeyPair. This is used to encrypt or decrypt traffic between
|
||||
* the client and server
|
||||
*
|
||||
* @return the server KeyPair
|
||||
*/
|
||||
public KeyPair getKeyPair() {
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a set of user who activated premium logins
|
||||
*
|
||||
* @return user who activated premium logins
|
||||
*/
|
||||
public Set<String> getEnabledPremium() {
|
||||
return enabledPremium;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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", 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)
|
||||
//check only for enabled plugins. A single plugin could be disabled by plugin managers
|
||||
&& 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 (errors) 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 to skip authentication from them
|
||||
getServer().getPluginManager().registerEvents(new PlayerListener(this, authPluginHook), this);
|
||||
return true;
|
||||
}
|
||||
}
|
@ -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,50 @@
|
||||
package com.github.games647.fastlogin;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Let users activate fast login by command. This only be accessible if
|
||||
* the user has access to it's account. So we can make sure that not another
|
||||
* person with a paid account and the same username can steal his account.
|
||||
*/
|
||||
public class PremiumCommand implements CommandExecutor {
|
||||
|
||||
private final FastLogin plugin;
|
||||
|
||||
public PremiumCommand(FastLogin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
if (!(sender instanceof Player)) {
|
||||
//console or command block
|
||||
sender.sendMessage(ChatColor.DARK_RED + "Only players can add themselves as premium");
|
||||
return true;
|
||||
}
|
||||
|
||||
String playerName = sender.getName();
|
||||
plugin.getEnabledPremium().add(playerName);
|
||||
sender.sendMessage(ChatColor.DARK_GREEN + "Added to the list of premium players");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sender.hasPermission(plugin.getName() + ".command." + command.getName() + ".others")) {
|
||||
String playerName = args[0];
|
||||
//todo check if valid username
|
||||
plugin.getEnabledPremium().add(playerName);
|
||||
sender.sendMessage(ChatColor.DARK_GREEN + "Added "
|
||||
+ ChatColor.DARK_BLUE + ChatColor.BOLD + playerName
|
||||
+ ChatColor.RESET + ChatColor.DARK_GREEN + " to the list of premium players");
|
||||
} else {
|
||||
sender.sendMessage(ChatColor.DARK_RED + "Not enough permissions");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.github.games647.fastlogin.hooks;
|
||||
|
||||
import fr.xephi.authme.api.NewAPI;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Github: https://github.com/Xephi/AuthMeReloaded/
|
||||
* Project page: http://dev.bukkit.org/bukkit-plugins/authme-reloaded/
|
||||
*/
|
||||
public class AuthMeHook implements AuthPlugin {
|
||||
|
||||
@Override
|
||||
public void forceLogin(Player 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,32 @@
|
||||
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
|
||||
//this automatically marks the player as logged in
|
||||
playerData = new LoginPlayerData(player);
|
||||
crazyDatabase.save(playerData);
|
||||
} else {
|
||||
//mark the account as logged in
|
||||
playerData.setLoggedIn(true);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
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 and restore location
|
||||
securityPlugin.rehabPlayer(player, name);
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
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);
|
||||
if (xAuthPlayer != null) {
|
||||
//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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,235 @@
|
||||
package com.github.games647.fastlogin.listener;
|
||||
|
||||
import com.comphenix.protocol.PacketType;
|
||||
import com.comphenix.protocol.ProtocolManager;
|
||||
import com.comphenix.protocol.events.PacketAdapter;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.wrappers.WrappedChatComponent;
|
||||
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
||||
import com.github.games647.fastlogin.Encryption;
|
||||
import com.github.games647.fastlogin.FastLogin;
|
||||
import com.github.games647.fastlogin.PlayerSession;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
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;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
/**
|
||||
* Handles incoming encryption responses from connecting clients.
|
||||
* It prevents them from reaching the server because that cannot handle
|
||||
* it in offline mode.
|
||||
*
|
||||
* Moreover this manages a started premium check from
|
||||
* this plugin. So check if all data is correct and we can prove him as a
|
||||
* owner of a paid minecraft account.
|
||||
*
|
||||
* Receiving packet information:
|
||||
* http://wiki.vg/Protocol#Encryption_Response
|
||||
*
|
||||
* sharedSecret=encrypted byte array
|
||||
* verify token=encrypted byte array
|
||||
*/
|
||||
public class EncryptionPacketListener extends PacketAdapter {
|
||||
|
||||
//mojang api check to prove a player is logged in minecraft and made a join server request
|
||||
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
|
||||
|
||||
private final ProtocolManager protocolManager;
|
||||
//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.plugin = plugin;
|
||||
this.protocolManager = protocolManger;
|
||||
}
|
||||
|
||||
/**
|
||||
* C->S : Handshake State=2
|
||||
* C->S : Login Start
|
||||
* S->C : Encryption Key Request
|
||||
* (Client Auth)
|
||||
* C->S : Encryption Key Response
|
||||
* (Server Auth, Both enable encryption)
|
||||
* S->C : Login Success (*)
|
||||
*
|
||||
* On offline logins is Login Start followed by Login Success
|
||||
*
|
||||
* Minecraft Server implementation
|
||||
* https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L180
|
||||
*/
|
||||
@Override
|
||||
public void onPacketReceiving(PacketEvent packetEvent) {
|
||||
PacketContainer packet = packetEvent.getPacket();
|
||||
Player player = packetEvent.getPlayer();
|
||||
|
||||
//the player name is unknown to ProtocolLib (so getName() doesn't work) - 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);
|
||||
//encrypted verify token
|
||||
byte[] clientVerify = packet.getByteArrays().read(1);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 = session.getUsername();
|
||||
if (hasJoinedServer(username, serverId)) {
|
||||
plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username);
|
||||
|
||||
session.setVerified(true);
|
||||
receiveFakeStartPacket(username, player);
|
||||
} else {
|
||||
//user tried to fake a authentication
|
||||
disconnect(packetEvent, "Invalid session", Level.FINE
|
||||
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
|
||||
, session.getUsername(), player.getAddress(), serverId);
|
||||
}
|
||||
|
||||
packetEvent.setCancelled(true);
|
||||
}
|
||||
|
||||
private void disconnect(PacketEvent packetEvent, String kickReason, Level logLevel, String logMessage
|
||||
, Object... arguments) {
|
||||
plugin.getLogger().log(logLevel, logMessage, arguments);
|
||||
kickPlayer(packetEvent.getPlayer(), kickReason);
|
||||
//cancel the event in order to prevent the server receiving an invalid packet
|
||||
packetEvent.setCancelled(true);
|
||||
}
|
||||
|
||||
private void kickPlayer(Player player, String reason) {
|
||||
PacketContainer kickPacket = protocolManager.createPacket(PacketType.Login.Server.DISCONNECT);
|
||||
kickPacket.getChatComponents().write(0, WrappedChatComponent.fromText(reason));
|
||||
|
||||
try {
|
||||
//send kick packet at login state
|
||||
//the normal event.getPlayer.kickPlayer(String) method does only work at play state
|
||||
protocolManager.sendServerPacket(player, kickPacket);
|
||||
//tell the server that we want to close the connection
|
||||
player.kickPlayer("Disconnect");
|
||||
} catch (InvocationTargetException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Error sending kickpacket", ex);
|
||||
}
|
||||
}
|
||||
|
||||
//try to get the networkManager from ProtocolLib
|
||||
private Object getNetworkManager(Player player)
|
||||
throws SecurityException, IllegalAccessException, NoSuchFieldException {
|
||||
Object injector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
|
||||
Field injectorField = injector.getClass().getDeclaredField("injector");
|
||||
injectorField.setAccessible(true);
|
||||
|
||||
Object rawInjector = injectorField.get(injector);
|
||||
|
||||
injectorField = rawInjector.getClass().getDeclaredField("networkManager");
|
||||
injectorField.setAccessible(true);
|
||||
return injectorField.get(rawInjector);
|
||||
}
|
||||
|
||||
private boolean hasJoinedServer(String username, String serverId) {
|
||||
try {
|
||||
String url = HAS_JOINED_URL + "username=" + username + "&serverId=" + serverId;
|
||||
|
||||
HttpURLConnection conn = plugin.getConnection(url);
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
String line = reader.readLine();
|
||||
if (!line.equals("null")) {
|
||||
//validate parsing
|
||||
//http://wiki.vg/Protocol_Encryption#Server
|
||||
JSONObject userData = (JSONObject) JSONValue.parseWithException(line);
|
||||
String uuid = (String) userData.get("id");
|
||||
String name = (String) userData.get("name");
|
||||
|
||||
JSONArray properties = (JSONArray) userData.get("properties");
|
||||
JSONObject skinData = (JSONObject) properties.get(0);
|
||||
//base64 encoded skin data
|
||||
String encodedSkin = (String) skinData.get("value");
|
||||
|
||||
return true;
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
|
||||
//this connection doesn't need to be closed. So can make use of keep alive in java
|
||||
return false;
|
||||
}
|
||||
|
||||
//fake a new login packet in order to let the server handle all the other stuff
|
||||
private void receiveFakeStartPacket(String username, Player from) {
|
||||
//see StartPacketListener for packet information
|
||||
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START, true);
|
||||
|
||||
//uuid is ignored
|
||||
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
|
||||
startPacket.getGameProfiles().write(0, fakeProfile);
|
||||
try {
|
||||
//we don't want to handle our own packets so ignore filters
|
||||
protocolManager.recieveClientPacket(from, startPacket, false);
|
||||
} catch (InvocationTargetException | IllegalAccessException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to fake a new start packet", ex);
|
||||
//cancel the event in order to prevent the server receiving an invalid packet
|
||||
kickPlayer(from, "Error occured");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package com.github.games647.fastlogin.listener;
|
||||
|
||||
import com.github.games647.fastlogin.FastLogin;
|
||||
import com.github.games647.fastlogin.PlayerSession;
|
||||
import com.github.games647.fastlogin.hooks.AuthPlugin;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
/**
|
||||
* This listener tells authentication plugins if the player
|
||||
* has a premium account and we checked it successfully. So the
|
||||
* plugin can skip authentication.
|
||||
*/
|
||||
public class PlayerListener implements Listener {
|
||||
|
||||
private static final long DELAY_LOGIN = 1 * 20L;
|
||||
|
||||
private final FastLogin plugin;
|
||||
private final AuthPlugin authPlugin;
|
||||
|
||||
public PlayerListener(FastLogin plugin, AuthPlugin authPlugin) {
|
||||
this.plugin = plugin;
|
||||
this.authPlugin = authPlugin;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onJoin(PlayerJoinEvent joinEvent) {
|
||||
final Player player = joinEvent.getPlayer();
|
||||
String address = player.getAddress().toString();
|
||||
|
||||
//removing the session because we now use it
|
||||
PlayerSession session = plugin.getSessions().remove(address);
|
||||
//check if it's the same player as we checked before
|
||||
if (session != null && player.getName().equals(session.getUsername()) && session.isVerified()) {
|
||||
Bukkit.getScheduler().runTaskLater(plugin, new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (player.isOnline()) {
|
||||
plugin.getLogger().log(Level.FINE, "Logging player {0} in", player.getName());
|
||||
authPlugin.forceLogin(player);
|
||||
}
|
||||
}
|
||||
//Wait before auth plugin initializes the player
|
||||
}, DELAY_LOGIN);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
package com.github.games647.fastlogin.listener;
|
||||
|
||||
import com.comphenix.protocol.PacketType;
|
||||
import com.comphenix.protocol.ProtocolManager;
|
||||
import com.comphenix.protocol.events.PacketAdapter;
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.github.games647.fastlogin.FastLogin;
|
||||
import com.github.games647.fastlogin.PlayerSession;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.security.PublicKey;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Handles incoming start packets from connecting clients. It
|
||||
* checks if we can start checking if the player is premium and
|
||||
* start a request to the client that it should start online mode
|
||||
* login.
|
||||
*
|
||||
* Receiving packet information:
|
||||
* http://wiki.vg/Protocol#Login_Start
|
||||
*
|
||||
* String=Username
|
||||
*/
|
||||
public class StartPacketListener extends PacketAdapter {
|
||||
|
||||
//only premium (paid account) users have a uuid from here
|
||||
private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
|
||||
//this includes a-zA-Z1-9_
|
||||
private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
|
||||
private static final int VERIFY_TOKEN_LENGTH = 4;
|
||||
|
||||
private final ProtocolManager protocolManager;
|
||||
//hides the inherit Plugin plugin field, but we need a more detailed type than just Plugin
|
||||
private final FastLogin plugin;
|
||||
|
||||
//just create a new once on plugin enable. This used for verify token generation
|
||||
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 are making api calls to Mojang
|
||||
super(params(plugin, PacketType.Login.Client.START).optionAsync());
|
||||
|
||||
this.plugin = plugin;
|
||||
this.protocolManager = protocolManger;
|
||||
}
|
||||
|
||||
/**
|
||||
* C->S : Handshake State=2
|
||||
* C->S : Login Start
|
||||
* S->C : Encryption Key Request
|
||||
* (Client Auth)
|
||||
* C->S : Encryption Key Response
|
||||
* (Server Auth, Both enable encryption)
|
||||
* S->C : Login Success (*)
|
||||
*
|
||||
* On offline logins is Login Start followed by Login Success
|
||||
*/
|
||||
@Override
|
||||
public void onPacketReceiving(PacketEvent packetEvent) {
|
||||
PacketContainer packet = packetEvent.getPacket();
|
||||
Player player = packetEvent.getPlayer();
|
||||
|
||||
//this includes ip:port. Should be unique for an incoming login request with a timeout of 2 minutes
|
||||
String sessionKey = player.getAddress().toString();
|
||||
|
||||
//remove old data every time on a new login in order to keep the session only for one person
|
||||
plugin.getSessions().remove(sessionKey);
|
||||
|
||||
//player.getName() won't work at this state
|
||||
String username = packet.getGameProfiles().read(0).getName();
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting to the server"
|
||||
, new Object[]{sessionKey, username});
|
||||
if (plugin.getEnabledPremium().contains(username) && isPremium(username)) {
|
||||
//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) {
|
||||
//check if it's a valid playername
|
||||
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;
|
||||
//204 - no content for not found
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to check if player has a paid account", ex);
|
||||
}
|
||||
//this connection doesn't need to be closed. So can make use of keep alive in java
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void sentEncryptionRequest(String sessionKey, String username, Player player, PacketEvent packetEvent) {
|
||||
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
|
||||
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());
|
||||
//generate a random token which should be the same when we receive it from the client
|
||||
byte[] verifyToken = new byte[VERIFY_TOKEN_LENGTH];
|
||||
random.nextBytes(verifyToken);
|
||||
newPacket.getByteArrays().write(0, verifyToken);
|
||||
|
||||
protocolManager.sendServerPacket(player, newPacket);
|
||||
|
||||
//cancel only if the player has a paid account otherwise login as normal offline player
|
||||
plugin.getSessions().put(sessionKey, new PlayerSession(verifyToken, username));
|
||||
packetEvent.setCancelled(true);
|
||||
} catch (InvocationTargetException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Cannot send encryption packet. Falling back to normal login", ex);
|
||||
}
|
||||
}
|
||||
}
|
33
src/main/resources/plugin.yml
Normal file
33
src/main/resources/plugin.yml
Normal file
@ -0,0 +1,33 @@
|
||||
# project informations for Bukkit in order to register our plugin with all it components
|
||||
# ${project.name} are variables from Maven (pom.xml) which will be replaced after the build
|
||||
name: ${project.name}
|
||||
version: ${project.version}
|
||||
main: ${project.groupId}.${project.artifactId}.${project.name}
|
||||
|
||||
# meta informations for plugin managers
|
||||
authors: [games647, 'https://github.com/games647/FastLogin/graphs/contributors']
|
||||
description: |
|
||||
${project.description}
|
||||
website: ${project.url}
|
||||
dev-url: ${project.url}
|
||||
|
||||
depend: [ProtocolLib]
|
||||
softdepend:
|
||||
- xAuth
|
||||
- AuthMe
|
||||
- CrazyLogin
|
||||
- LoginSecurity
|
||||
|
||||
commands:
|
||||
${project.artifactId}.:
|
||||
description: 'Marks the invoker or the player specified as premium'
|
||||
aliases: [prem, premium, loginfast]
|
||||
usage: /<command> [player]
|
||||
permission: ${project.artifactId}.command.premium
|
||||
|
||||
permissions:
|
||||
${project.artifactId}.command.premium:
|
||||
description: 'Mark themselves as premium using a command'
|
||||
default: true
|
||||
${project.artifactId}.command.premium.others:
|
||||
description: 'Mark other people as premium'
|
Reference in New Issue
Block a user