Compare commits

..

5 Commits
0.1 ... 0.2.2

20 changed files with 761 additions and 404 deletions

View File

@@ -1,7 +1,13 @@
# mcMMOExtras
# FastLogin
A visual boss bar Bukkit plugin for mcMMO that keeps people entertained and encourages them to want to level up.
Checks if a minecraft player has a valid premium (paid account). If so, they can skip offline authentification.
See
* http://dev.bukkit.org/bukkit-plugins/mcmmoextras/
* http://www.curse.com/bukkit-plugins/minecraft/mcmmoextras
Requirements:
* [ProtocolLib](http://www.spigotmc.org/resources/protocollib.1997/)
* Bukkit 1.8.8
* Java 8 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/)

View File

@@ -1,198 +0,0 @@
Options:
ConfigurationVersion_DO_NOT_CHANGE_THIS: 6
# 0 -> unlimited/as needed.
NettyThreads: 0
# DON'T USE THAT IF YOU CAN USE SOCKETS, Enable when you can't use sockets but you want use that on bungee server, it isn't good as sockets, but it should not explode. NOTE: You NEED MySQL for that, and this is slower and may by VERY buggy on first join.
UseChannels: false
# Time in ms between trying connect LISTENER to HANLDER socket, on linux using smaller times can cause some weird problems.
ReTryConnectSocketTime: 250
# where AutoIn should store exceptions, enabled login, whitelists, cache and other player settings.
SaveData:
Type: MySQL
MySQL:
Host: localhost
Port: 3306
User: minecraft
Pass:
Database: minecraft
Prefix: AutoIn_
SQLite:
File: AutoIn_PlayerOptions.db
# Both
Both:
Dependencies:
# If you have one of that plugin, you can force AutoIn to don't use them even if they can be used. (change to true)
ForceDisable:
ProtocolLib: false
Skript: false
AuthMe: false
LogIt: false
XAuth: false
LoginSecurity: false
Listeners:
# you can try disable that and manualy configure priority below if auto-login will stop work after update of auth pluhin. PS: tell me about that problem!
GetFromModule: true
PlayerLoginEvent: LOWEST
PlayerJoinEvent: LOWEST
PlayerQuitEvent: MONITOR
Players:
# no more /login commands for premium users! Enable only on servers where you have auth plugin!
AutoLogin: true
# if true autoin will try fix skins, you can disable that if you want use other plugin. (some plugins may still not work)
FixSkins: true
# if true, then all players are exception by default, they can use /ai IAmNowPremium to remove exception flag (NOTE: they will lose all data after login as premium if you have fixedUUIDs set to false)
NegateExceptions: false
# if true, then even premium players needs to register. PS: You can make registration optional, see wiki: https://github.com/GotoFinal/AutoIn/wiki/Registration
Registration: true
# A.K.A. SwitchMode, If you disable that, only old cracked players will be able to join. New cracked players will be kicked from server. Good if you want switch from offline-mode to online-mode without losing players!
AllowNewCrackedPlayers: true
# if true, then everyone have UUID generated from nickname
FixedUUID: false
# WhiteList that works only on cracked players, black-list works even if this is disabled.
CrackedWhiteListEnabled: false
ForceLogin:
# Allow use RegEx in nicknames list, like 'Test\\d{1,3}'
UseRegEx: false
# Nicknames from this list don't need use login or register command, use to supprot mods like buildcraft and others
Nicknames:
- SomeNicknameThatDoNotNeedUseLoginOrRegisterCommandEvenIfHeDoNotHavePremium
Sessions:
# If enabled, plugin will remember premium players with their IP number, when servers will be down, premium players can be still auto-logged if IP will be valid.
Enabled: true
# If true, then players with valid session can join even if mojang server are down and without using password. PS: You can enable this and ServerProtect, then registered players OR players with valid session can join.
AsServerProtect: true
# time in easy format, 1w = 1 week, 1d = 1 day, 1d5h12s -> 1 day, 5 hours, 12 seconds
Expires: 1d
# If true sessions aren't saved on reload.
ExpiresOnRestart: true
Protections:
# If enabled, when new cracked player join to game he will get special protection "flag", that work like exception-player. so even if someone buy premium account with this same nickname, he will be still tract as cracked player.
# If player will buy premium, he must disable protection using "IamNowPremium" command, or admin must disable it manually, using API or /setProtection command.
CrackedPlayersNicknames:
Enabled: true
# If disabled, player must use "ProtectMe" command, or protection must be enabled manually, using API or /setProtection command
AutoProtect: true
ServerProtect:
# If enabled and mojang server don't work, only registered players can join (So you must set "Registration" to true). If disabled, all players will be kicked.
Enabled: false
# If enabled plugin will remember if username was premium. (It's saved to database/file) NOTE: Enable that if you have bigger server, like 100/200+ players online.
Cache: true
# Server
# ignored by proxy plugin
Server:
# It will try check if cache is updated before player join.
UpdateCacheOnPreJoin: false
# It will try check if cache is updated after player join.
UpdateCacheOnJoin: true
# Proxy
# ignored by server
Proxy:
# Players needs to be logged in to use commands
NeedLoginToUseCommands: true
# Player can use this commands
NotBlocked:
- /login
- /register
- /l
# players from selected group can be send to other servers. This override Proxy/BungeeCord setting!
ForcedServers:
PREMIUM:
Enabled: false
# works like "default_server" from bungee
Server: PremiumServer
# works like "force_default_server" from bungee
Force: false
# works like "forced_hosts" from bungee -> this override "Force" option
ForcedHosts:
eg: nul.goto. ...\\.. .final..IIlIlIIlIl@71c66aab
# like "fallback_server" from bungee
Fallback: FallbackServer
CRACKED:
Enabled: true
# works like "default_server" from bungee
Server: CrackedServer
# works like "force_default_server" from bungee
Force: true
# like "fallback_server" from bungee
Fallback: FallbackServer
EXCEPTION:
Enabled: false
# works like "default_server" from bungee
Server: PremiumServer
# works like "force_default_server" from bungee
Force: true
# like "fallback_server" from bungee
Fallback: FallbackServer
# This status will use this same settings as parent status, but you can override that settings by adding them here.
Parent: CRACKED
ERROR:
Enabled: false
# works like "default_server" from bungee
Server: ErrorServer
# works like "force_default_server" from bungee
Force: false
# like "fallback_server" from bungee
Fallback: FallbackServer

BIN
lib/CrazyCore v10.7.7.jar Normal file

Binary file not shown.

BIN
lib/CrazyLogin v7.23.2.jar Normal file

Binary file not shown.

Binary file not shown.

40
pom.xml
View File

@@ -8,11 +8,11 @@
<packaging>jar</packaging>
<name>FastLogin</name>
<version>0.1</version>
<version>0.2.2</version>
<inceptionYear>2015</inceptionYear>
<url>http://dev.bukkit.org/bukkit-plugins/fastlogin</url>
<url>https://github.com/games647/FastLogin</url>
<description>
Automatically logins premium player on a offline mode server
Automatically logins premium (paid accounts) player on a offline mode server
</description>
<properties>
@@ -43,9 +43,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<!--So many people still use Java 6 ;( http://mcstats.org/global/#Java+Version-->
<source>1.8</source>
<target>1.8</target>
<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-->
@@ -110,7 +109,7 @@
<!--Server API-->
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot</artifactId>
<artifactId>spigot-api</artifactId>
<version>1.8.8-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
@@ -119,7 +118,7 @@
<dependency>
<groupId>com.comphenix.protocol</groupId>
<artifactId>ProtocolLib</artifactId>
<version>3.6.3-SNAPSHOT</version>
<version>3.6.5-SNAPSHOT</version>
<optional>true</optional>
</dependency>
@@ -134,6 +133,7 @@
<groupId>de.luricos.bukkit</groupId>
<artifactId>xAuth</artifactId>
<version>2.6</version>
<!--These artifacts produce conflicts on downloading-->
<exclusions>
<exclusion>
<groupId>net.gravitydevelopment.updater</groupId>
@@ -145,5 +145,29 @@
</exclusion>
</exclusions>
</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>
</project>

View File

@@ -0,0 +1,125 @@
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;
/**
* 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
}
}

View File

@@ -3,91 +3,162 @@ package com.github.games647.fastlogin;
import com.github.games647.fastlogin.listener.PlayerListener;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.utility.SafeCacheBuilder;
import com.github.games647.fastlogin.hooks.AuthPlugin;
import com.github.games647.fastlogin.listener.EncryptionPacketListener;
import com.github.games647.fastlogin.listener.StartPacketListener;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.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.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
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;
public class FastLogin extends JavaPlugin {
private final KeyPair keyPair = generateKey();
private final Cache<String, PlayerData> session = CacheBuilder.newBuilder()
private static final int TIMEOUT = 15000;
private static final String USER_AGENT = "Premium-Checker";
//provide a immutable key pair to be thread safe
private final KeyPair keyPair = Encryption.generateKeyPair();
//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()
//mapped by ip:port
.expireAfterWrite(2, TimeUnit.MINUTES)
.build();
//2 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
.build(new CacheLoader<String, PlayerSession>() {
@Override
public void onEnable() {
if (!isEnabled()) {
return;
}
if (!getServer().getPluginManager().isPluginEnabled("AuthMe")
&& !getServer().getPluginManager().isPluginEnabled("xAuth")) {
getLogger().warning("No support offline Auth plugin found. ");
getLogger().warning("Disabling this plugin...");
setEnabled(false);
return;
}
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager));
protocolManager.addPacketListener(new StartPacketListener(this, protocolManager));
getServer().getPluginManager().registerEvents(new PlayerListener(this), this);
}
@Override
public PlayerSession load(String key) throws Exception {
//A key should be inserted manually on start packet
throw new UnsupportedOperationException("Not supported");
}
});
@Override
public void onLoad() {
//online mode is only changeable aftter a restart
//online mode is only changeable after a restart so check it here
if (getServer().getOnlineMode()) {
getLogger().severe("Server have to be in offline mode");
setEnabled(false);
}
generateKey();
}
private KeyPair generateKey() {
try {
KeyPairGenerator keypairgenerator = KeyPairGenerator.getInstance("RSA");
keypairgenerator.initialize(1024);
return keypairgenerator.generateKeyPair();
} catch (NoSuchAlgorithmException noSuchAlgorithmException) {
//Should be default existing in every vm
@Override
public void onEnable() {
if (!isEnabled() || !registerHooks()) {
return;
}
return null;
//register packet listeners on success
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager));
protocolManager.addPacketListener(new StartPacketListener(this, protocolManager));
//register commands
getCommand("premium").setExecutor(new PremiumCommand(this));
}
public Cache<String, PlayerData> getSession() {
@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
*
* @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 {
final HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(TIMEOUT);
connection.setReadTimeout(TIMEOUT);
//the new Mojang API just uses json as response
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", "Premium-Checker");
connection.setRequestProperty("User-Agent", USER_AGENT);
return connection;
}
private boolean registerHooks() {
AuthPlugin authPluginHook = null;
try {
String hooksPackage = this.getClass().getPackage().getName() + ".hooks";
//Look through all classes in the hooks package and look for supporting plugins on the server
for (ClassPath.ClassInfo clazzInfo : ClassPath.from(getClassLoader()).getTopLevelClasses(hooksPackage)) {
//remove the hook suffix
String pluginName = clazzInfo.getSimpleName().replace("Hook", "");
Class<?> clazz = clazzInfo.load();
//uses only member classes which uses AuthPlugin interface (skip interfaces)
if (AuthPlugin.class.isAssignableFrom(clazz)
&& getServer().getPluginManager().isPluginEnabled(pluginName)) {
authPluginHook = (AuthPlugin) clazz.newInstance();
getLogger().log(Level.INFO, "Hooking into auth plugin: {0}", pluginName);
break;
}
}
} catch (InstantiationException | IllegalAccessException | IOException ex) {
getLogger().log(Level.SEVERE, "Couldn't load the integration class", ex);
}
if (authPluginHook == null) {
//run this check for exceptions and not found plugins
getLogger().warning("No support offline Auth plugin found. ");
getLogger().warning("Disabling this plugin...");
setEnabled(false);
return false;
}
//We found a supporting plugin - we can now register a forwarding listener
getServer().getPluginManager().registerEvents(new PlayerListener(this, authPluginHook), this);
return true;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
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;
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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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
securityPlugin.rehabPlayer(player, name);
}
}

View File

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

View File

@@ -5,17 +5,18 @@ 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.SocketInjector;
import com.comphenix.protocol.injector.server.TemporaryPlayerFactory;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.github.games647.fastlogin.Encryption;
import com.github.games647.fastlogin.FastLogin;
import com.github.games647.fastlogin.PlayerData;
import com.github.games647.fastlogin.PlayerSession;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.security.PrivateKey;
@@ -24,29 +25,35 @@ import java.util.logging.Level;
import javax.crypto.SecretKey;
import net.minecraft.server.v1_8_R3.MinecraftEncryption;
import net.minecraft.server.v1_8_R3.NetworkManager;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
/**
* Receiving packet information:
* http://wiki.vg/Protocol#Encryption_Response
*
* sharedSecret=encrypted byte array
* verify token=encrypted byte array
*/
public class EncryptionPacketListener extends PacketAdapter {
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
private final ProtocolManager protocolManager;
private final FastLogin fastLogin;
//hides the inherit Plugin plugin field, but we need this type
private final FastLogin plugin;
public EncryptionPacketListener(FastLogin plugin, ProtocolManager protocolManger) {
//run async in order to not block the server, because we make api calls to Mojang
super(params(plugin, PacketType.Login.Client.ENCRYPTION_BEGIN).optionAsync());
this.fastLogin = plugin;
this.plugin = plugin;
this.protocolManager = protocolManger;
}
/*
/**
* C->S : Handshake State=2
* C->S : Login Start
* S->C : Encryption Key Request
@@ -54,92 +61,141 @@ public class EncryptionPacketListener extends PacketAdapter {
* C->S : Encryption Key Response
* (Server Auth, Both enable encryption)
* S->C : Login Success (*)
*
* On offline logins is Login Start followed by Login Success
*
* Minecraft Server implementation
* https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L180
*/
@Override
public void onPacketReceiving(PacketEvent event) {
PacketContainer packet = event.getPacket();
Player player = event.getPlayer();
public void onPacketReceiving(PacketEvent packetEvent) {
PacketContainer packet = packetEvent.getPacket();
Player player = packetEvent.getPlayer();
final byte[] sharedSecret = packet.getByteArrays().read(0);
//the player name is unknown to ProtocolLib - now uses ip:port as key
String uniqueSessionKey = player.getAddress().toString();
PlayerSession session = plugin.getSessions().get(uniqueSessionKey);
if (session == null) {
disconnect(packetEvent, "Invalid request", Level.FINE
, "Player {0} tried to send encryption response"
+ "on an invalid connection state"
, player.getAddress());
return;
}
byte[] sharedSecret = packet.getByteArrays().read(0);
byte[] clientVerify = packet.getByteArrays().read(1);
PrivateKey privateKey = fastLogin.getKeyPair().getPrivate();
String addressString = player.getAddress().toString();
PlayerData cachedEntry = fastLogin.getSession().asMap().get(addressString);
byte[] serverVerify = cachedEntry.getVerifyToken();
if (!Arrays.equals(serverVerify, MinecraftEncryption.b(privateKey, clientVerify))) {
player.kickPlayer("Invalid token");
event.setCancelled(true);
PrivateKey privateKey = plugin.getKeyPair().getPrivate();
byte[] serverVerify = session.getVerifyToken();
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L182
if (!Arrays.equals(serverVerify, Encryption.decryptData(privateKey, clientVerify))) {
//check if the verify token are equal to the server sent one
disconnect(packetEvent, "Invalid token", Level.FINE
, "Player {0} ({1}) tried to login with an invalid verify token. "
+ "Server: {2} Client: {3}"
, session.getUsername(), player.getAddress(), serverVerify, clientVerify);
return;
}
//encrypt all following packets
NetworkManager networkManager = getNetworkManager(event);
SecretKey loginKey = MinecraftEncryption.a(privateKey, sharedSecret);
networkManager.a(loginKey);
String serverId = (new BigInteger(MinecraftEncryption.a("", fastLogin.getKeyPair().getPublic(), loginKey)))
SecretKey loginKey = Encryption.decryptSharedKey(privateKey, sharedSecret);
try {
//get the NMS connection handle of this player
Object networkManager = getNetworkManager(player);
//try to detect the method by parameters
Method encryptConnectionMethod = FuzzyReflection.fromObject(networkManager)
.getMethodByParameters("a", SecretKey.class);
//encrypt/decrypt following packets
//the client expects this behaviour
encryptConnectionMethod.invoke(networkManager, loginKey);
} catch (ReflectiveOperationException ex) {
disconnect(packetEvent, "Error occurred", Level.SEVERE, "Couldn't enable encryption", ex);
return;
}
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L193
//generate the server id based on client and server data
String serverId = (new BigInteger(Encryption.getServerIdHash("", plugin.getKeyPair().getPublic(), loginKey)))
.toString(16);
String username = cachedEntry.getUsername();
if (!hasJoinedServer(username, serverId)) {
String username = session.getUsername();
if (hasJoinedServer(username, serverId)) {
session.setVerified(true);
plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username);
receiveFakeStartPacket(username, player);
} else {
//user tried to fake a authentification
player.kickPlayer("Invalid session");
event.setCancelled(true);
return;
disconnect(packetEvent, "Invalid session", Level.FINE
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
, session.getUsername(), player.getAddress(), serverId);
}
//fake a new login packet
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START, true);
WrappedGameProfile fakeProfile = WrappedGameProfile.fromOfflinePlayer(Bukkit.getOfflinePlayer(username));
startPacket.getGameProfiles().write(0, fakeProfile);
try {
protocolManager.recieveClientPacket(event.getPlayer(), startPacket, false);
} catch (InvocationTargetException | IllegalAccessException ex) {
plugin.getLogger().log(Level.WARNING, null, ex);
}
event.setCancelled(true);
packetEvent.setCancelled(true);
}
private NetworkManager getNetworkManager(PacketEvent event) throws IllegalArgumentException {
SocketInjector injector = TemporaryPlayerFactory.getInjectorFromPlayer(event.getPlayer());
NetworkManager networkManager = null;
try {
Field declaredField = injector.getClass().getDeclaredField("injector");
declaredField.setAccessible(true);
private void disconnect(PacketEvent packetEvent, String kickMessage, Level logLevel, String logMessage
, Object... arguments) {
plugin.getLogger().log(logLevel, logMessage, arguments);
packetEvent.getPlayer().kickPlayer(kickMessage);
//cancel the event in order to prevent the server receiving an invalid packet
packetEvent.setCancelled(true);
}
Object rawInjector = declaredField.get(injector);
private Object getNetworkManager(Player player)
throws SecurityException, IllegalAccessException, NoSuchFieldException {
Object injector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
Field injectorField = injector.getClass().getDeclaredField("injector");
injectorField.setAccessible(true);
declaredField = rawInjector.getClass().getDeclaredField("networkManager");
declaredField.setAccessible(true);
networkManager = (NetworkManager) declaredField.get(rawInjector);
} catch (IllegalAccessException | NoSuchFieldException ex) {
plugin.getLogger().log(Level.WARNING, null, ex);
}
Object rawInjector = injectorField.get(injector);
return networkManager;
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 = fastLogin.getConnection(url);
HttpURLConnection conn = plugin.getConnection(url);
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = reader.readLine();
if (!line.equals("null")) {
JSONObject object = (JSONObject) JSONValue.parse(line);
//validate parsing
JSONObject object = (JSONObject) JSONValue.parseWithException(line);
String uuid = (String) object.get("id");
String name = (String) object.get("name");
return true;
}
} catch (IOException ex) {
plugin.getLogger().log(Level.WARNING, null, ex);
} catch (Exception ex) {
//catch not only ioexceptions also parse and NPE on unexpected json format
plugin.getLogger().log(Level.WARNING, "Failed to verify if session is valid", ex);
}
//this connection doesn't need to be closed. So can make use of keep alive in java
return false;
}
private void receiveFakeStartPacket(String username, Player from) {
//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(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
from.kickPlayer("Error occurred");
}
}
}

View File

@@ -1,15 +1,10 @@
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 de.luricos.bukkit.xAuth.xAuth;
import de.luricos.bukkit.xAuth.xAuthPlayer;
import de.luricos.bukkit.xAuth.xAuthPlayer.Status;
import fr.xephi.authme.api.NewAPI;
import fr.xephi.authme.cache.limbo.LimboCache;
import java.sql.Timestamp;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -20,40 +15,39 @@ import org.bukkit.event.player.PlayerJoinEvent;
public class PlayerListener implements Listener {
private final FastLogin plugin;
private final AuthPlugin authPlugin;
public PlayerListener(FastLogin plugin) {
public PlayerListener(FastLogin plugin, AuthPlugin authPlugin) {
this.plugin = plugin;
this.authPlugin = authPlugin;
}
@EventHandler(ignoreCancelled = true)
public void onJoin(PlayerJoinEvent joinEvent) {
final Player player = joinEvent.getPlayer();
String address = player.getAddress().toString();
if (plugin.getSession().asMap().containsKey(address)) {
Bukkit.getScheduler().runTaskLater(plugin, () -> {
doLogin(player);
//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 && session.getUsername().equals(player.getName())
&& session.isVerified()) {
// Bukkit.getScheduler().runTaskLater(plugin, () -> {
// if (player.isOnline()) {
// plugin.getLogger().log(Level.FINER, "Logging player {0} in", player.getName());
// authPlugin.forceLogin(player);
// }
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
}, 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);
}
}
}

View File

@@ -6,7 +6,7 @@ import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.github.games647.fastlogin.FastLogin;
import com.github.games647.fastlogin.PlayerData;
import com.github.games647.fastlogin.PlayerSession;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
@@ -15,27 +15,41 @@ import java.security.PublicKey;
import java.util.Random;
import java.util.logging.Level;
import java.util.regex.Pattern;
import org.bukkit.entity.Player;
/**
* Receiving packet information:
* http://wiki.vg/Protocol#Login_Start
*
* String=Username
*/
public class StartPacketListener extends PacketAdapter {
//only premium members have a uuid from there
//only premium (paid account) users have a uuid from there
private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
//this includes a-zA-Z1-9_
private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
private final ProtocolManager protocolManager;
private final FastLogin fastLogin;
//hides the inherit Plugin plugin field, but we need a more detailed type than just Plugin
private final FastLogin plugin;
//just create a new once on plugin enable
private final Random random = new Random();
//compile the pattern on plugin enable
private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
public StartPacketListener(FastLogin plugin, ProtocolManager protocolManger) {
//run async in order to not block the server, because we make api calls to Mojang
super(params(plugin, PacketType.Login.Client.START).optionAsync());
this.fastLogin = plugin;
this.plugin = plugin;
this.protocolManager = protocolManger;
}
/*
/**
* C->S : Handshake State=2
* C->S : Login Start
* S->C : Encryption Key Request
@@ -43,48 +57,75 @@ public class StartPacketListener extends PacketAdapter {
* C->S : Encryption Key Response
* (Server Auth, Both enable encryption)
* S->C : Login Success (*)
*
* On offline logins is Login Start followed by Login Success
*/
@Override
public void onPacketReceiving(PacketEvent packetEvent) {
PacketContainer packet = packetEvent.getPacket();
Player player = packetEvent.getPlayer();
//this includes ip and port. Should be unique for 2 Minutes
String sessionKey = player.getAddress().toString();
//remove old data every time on a new login in order to keep the session only for one person
plugin.getSessions().remove(sessionKey);
String username = packet.getGameProfiles().read(0).getName();
plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting to the server"
, new Object[]{sessionKey, username});
//do premium login process
if (isPremium(username)) {
//do premium login process
try {
PacketContainer newPacket = protocolManager.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN, true);
//constr ServerID=""
//public key=plugin.getPublic
newPacket.getSpecificModifier(PublicKey.class).write(0, fastLogin.getKeyPair().getPublic());
byte[] verifyToken = new byte[4];
random.nextBytes(verifyToken);
newPacket.getByteArrays().write(0, verifyToken);
String addressString = player.getAddress().toString();
fastLogin.getSession().asMap().put(addressString, new PlayerData(verifyToken, username));
protocolManager.sendServerPacket(player, newPacket, false);
} catch (InvocationTargetException ex) {
plugin.getLogger().log(Level.SEVERE, null, ex);
}
//cancel only if the player is premium
packetEvent.setCancelled(true);
//minecraft server implementation
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
sentEncryptionRequest(sessionKey, username, player, packetEvent);
}
}
private boolean isPremium(String playerName) {
try {
final HttpURLConnection connection = fastLogin.getConnection(UUID_LINK + playerName);
final int responseCode = connection.getResponseCode();
//check if it's a valid playername and the user activated fast logins
if (playernameMatcher.matcher(playerName).matches() && plugin.getEnabledPremium().contains(playerName)) {
//only make a API call if the name is valid existing mojang account
try {
HttpURLConnection connection = plugin.getConnection(UUID_LINK + playerName);
int responseCode = connection.getResponseCode();
return responseCode == HttpURLConnection.HTTP_OK;
} catch (IOException ex) {
plugin.getLogger().log(Level.SEVERE, null, ex);
return responseCode == HttpURLConnection.HTTP_OK;
//204 - no content for not found
} catch (IOException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to check if player has a paid account", ex);
}
//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());
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);
}
}
}

View File

@@ -5,10 +5,29 @@ version: ${project.version}
main: ${project.groupId}.${project.artifactId}.${project.name}
# meta informations for plugin managers
authors: [Xeroun, games647, 'https://github.com/games647/FastLogin/graphs/contributors']
authors: [games647, 'https://github.com/games647/FastLogin/graphs/contributors']
description: |
${project.description}
website: ${project.url}
dev-url: ${project.url}
depend: [ProtocolLib]
depend: [ProtocolLib]
softdepend:
- xAuth
- AuthMe
- CrazyLogin
- LoginSecurity
commands:
premium:
description: 'Marks the invoker or the player specified as premium'
aliases: [prem, fastlogin, 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'