Compare commits

..

4 Commits
0.2.1 ... 0.2.3

13 changed files with 155 additions and 74 deletions

View File

@@ -1,11 +1,19 @@
# FastLogin
Checks if a minecraft player has a valid premium (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.
Requirements:
###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/)
* Bukkit 1.8.8
* Java 8 or above
* 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/)

15
pom.xml
View File

@@ -8,9 +8,9 @@
<packaging>jar</packaging>
<name>FastLogin</name>
<version>0.2</version>
<version>0.2.3</version>
<inceptionYear>2015</inceptionYear>
<url>http://dev.bukkit.org/bukkit-plugins/fastlogin</url>
<url>https://github.com/games647/FastLogin</url>
<description>
Automatically logins premium (paid accounts) player on a offline mode server
</description>
@@ -43,8 +43,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<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-->
@@ -127,12 +127,14 @@
<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>
@@ -146,10 +148,12 @@
</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>
@@ -158,14 +162,17 @@
<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>

View File

@@ -24,6 +24,9 @@ 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

View File

@@ -22,12 +22,17 @@ 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 {
private static final int TIMEOUT = 15000;
//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
//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
@@ -36,9 +41,9 @@ public class FastLogin extends JavaPlugin {
//this map is thread-safe for async access (Packet Listener)
//SafeCacheBuilder is used in order to be version independent
private final ConcurrentMap<String, PlayerSession> session = SafeCacheBuilder.<String, PlayerSession>newBuilder()
//mapped by ip:port
.expireAfterWrite(2, TimeUnit.MINUTES)
//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
@@ -52,6 +57,7 @@ public class FastLogin extends JavaPlugin {
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);
@@ -69,7 +75,7 @@ public class FastLogin extends JavaPlugin {
protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager));
protocolManager.addPacketListener(new StartPacketListener(this, protocolManager));
//register commands
//register commands using a unique name
getCommand("premium").setExecutor(new PremiumCommand(this));
}
@@ -81,8 +87,8 @@ public class FastLogin extends JavaPlugin {
}
/**
* Gets a thread-safe map about players which are connecting to the server are being checked to be premium (paid
* account)
* 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
*/
@@ -91,7 +97,8 @@ public class FastLogin extends JavaPlugin {
}
/**
* Gets the server KeyPair
* Gets the server KeyPair. This is used to encrypt or decrypt traffic between
* the client and server
*
* @return the server KeyPair
*/
@@ -138,6 +145,7 @@ public class FastLogin extends JavaPlugin {
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);
@@ -149,7 +157,7 @@ public class FastLogin extends JavaPlugin {
}
if (authPluginHook == null) {
//run this check for exceptions and not found plugins
//run this check for exceptions (errors) and not found plugins
getLogger().warning("No support offline Auth plugin found. ");
getLogger().warning("Disabling this plugin...");
@@ -157,7 +165,7 @@ public class FastLogin extends JavaPlugin {
return false;
}
//We found a supporting plugin - we can now register a forwarding listener
//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;
}

View File

@@ -6,6 +6,11 @@ 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;

View File

@@ -1,24 +1,17 @@
package com.github.games647.fastlogin.hooks;
import fr.xephi.authme.api.NewAPI;
import fr.xephi.authme.cache.limbo.LimboCache;
import org.bukkit.entity.Player;
/**
* Github: https://github.com/Xephi/AuthMeReloaded/
* Project page: dev.bukkit.org/bukkit-plugins/authme-reloaded/
* 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

@@ -21,6 +21,7 @@ public class CrazyLoginHook implements AuthPlugin {
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 {

View File

@@ -8,7 +8,8 @@ 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
* 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 {
@@ -23,7 +24,7 @@ public class LoginSecurityHook implements AuthPlugin {
securityPlugin.authList.remove(name);
//cancel timeout timer
securityPlugin.thread.timeout.remove(name);
//remove effects
//remove effects and restore location
securityPlugin.rehabPlayer(player, name);
}
}

View File

@@ -19,18 +19,20 @@ public class xAuthHook implements AuthPlugin {
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());
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()));
//update last login time
xAuthPlayer.setLoginTime(new Timestamp(System.currentTimeMillis()));
//mark the player as logged in
xAuthPlayer.setStatus(Status.AUTHENTICATED);
//mark the player as logged in
xAuthPlayer.setStatus(Status.AUTHENTICATED);
//restore inventory
xAuthPlugin.getPlayerManager().unprotect(xAuthPlayer);
//restore inventory
xAuthPlugin.getPlayerManager().unprotect(xAuthPlayer);
}
}
}

View File

@@ -7,6 +7,7 @@ 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;
@@ -21,16 +22,25 @@ 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.Bukkit;
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
*
@@ -39,6 +49,7 @@ import org.json.simple.JSONValue;
*/
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;
@@ -72,18 +83,18 @@ public class EncryptionPacketListener extends PacketAdapter {
PacketContainer packet = packetEvent.getPacket();
Player player = packetEvent.getPlayer();
//the player name is unknown to ProtocolLib - now uses ip:port as key
//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 {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();
@@ -122,13 +133,12 @@ public class EncryptionPacketListener extends PacketAdapter {
String username = session.getUsername();
if (hasJoinedServer(username, serverId)) {
session.setVerified(true);
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 authentification
//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);
@@ -137,14 +147,30 @@ public class EncryptionPacketListener extends PacketAdapter {
packetEvent.setCancelled(true);
}
private void disconnect(PacketEvent packetEvent, String kickMessage, Level logLevel, String logMessage
private void disconnect(PacketEvent packetEvent, String kickReason, Level logLevel, String logMessage
, Object... arguments) {
plugin.getLogger().log(logLevel, logMessage, arguments);
packetEvent.getPlayer().kickPlayer(kickMessage);
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);
@@ -168,9 +194,15 @@ public class EncryptionPacketListener extends PacketAdapter {
String line = reader.readLine();
if (!line.equals("null")) {
//validate parsing
JSONObject object = (JSONObject) JSONValue.parseWithException(line);
String uuid = (String) object.get("id");
String name = (String) object.get("name");
//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;
}
@@ -178,24 +210,26 @@ public class EncryptionPacketListener extends PacketAdapter {
//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
//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) {
//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));
//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
from.kickPlayer("Error occurred");
kickPlayer(from, "Error occured");
}
}
}

View File

@@ -12,7 +12,14 @@ 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;
@@ -30,15 +37,18 @@ public class PlayerListener implements Listener {
//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);
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
}, 1 * 20L);
}, DELAY_LOGIN);
}
}
}

View File

@@ -20,6 +20,11 @@ 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
*
@@ -27,21 +32,23 @@ import org.bukkit.entity.Player;
*/
public class StartPacketListener extends PacketAdapter {
//only premium (paid account) users have a uuid from there
//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
//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 make api calls to Mojang
//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;
@@ -64,17 +71,17 @@ public class StartPacketListener extends PacketAdapter {
PacketContainer packet = packetEvent.getPacket();
Player player = packetEvent.getPlayer();
//this includes ip and port. Should be unique for 2 Minutes
//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});
//do premium login process
if (isPremium(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);
@@ -82,8 +89,8 @@ public class StartPacketListener extends PacketAdapter {
}
private boolean isPremium(String playerName) {
//check if it's a valid playername and the user activated fast logins
if (playernameMatcher.matcher(playerName).matches() && plugin.getEnabledPremium().contains(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);
@@ -114,15 +121,16 @@ public class StartPacketListener extends PacketAdapter {
.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN, true);
newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getKeyPair().getPublic());
byte[] verifyToken = new byte[4];
//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, false);
protocolManager.sendServerPacket(player, newPacket);
//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));
packetEvent.setCancelled(true);
} catch (InvocationTargetException ex) {
plugin.getLogger().log(Level.SEVERE, "Cannot send encryption packet. Falling back to normal login", ex);
}

View File

@@ -19,10 +19,11 @@ softdepend:
- LoginSecurity
commands:
premium:
${project.artifactId}.:
description: 'Marks the invoker or the player specified as premium'
aliases: [prem, fastlogin, loginfast]
aliases: [prem, premium, loginfast]
usage: /<command> [player]
permission: ${project.artifactId}.command.premium
permissions:
${project.artifactId}.command.premium: