mirror of
https://github.com/TuxCoding/FastLogin.git
synced 2025-07-30 10:47:33 +02:00
Fix thread safety for fake start packets (Bukkit.getOfflinePlayer doesn't look like to be thread-safe) + More documentation
This commit is contained in:
@ -24,6 +24,8 @@ import javax.crypto.spec.IvParameterSpec;
|
|||||||
import javax.crypto.spec.SecretKeySpec;
|
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
|
* Source: https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/MinecraftEncryption.java
|
||||||
*
|
*
|
||||||
* Remapped by: https://github.com/Techcable/MinecraftMappings/tree/master/1.8
|
* Remapped by: https://github.com/Techcable/MinecraftMappings/tree/master/1.8
|
||||||
|
@ -22,12 +22,17 @@ import java.util.logging.Level;
|
|||||||
|
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
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 {
|
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";
|
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();
|
private final KeyPair keyPair = Encryption.generateKeyPair();
|
||||||
|
|
||||||
//we need a thread-safe set because we access it async in the packet listener
|
//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)
|
//this map is thread-safe for async access (Packet Listener)
|
||||||
//SafeCacheBuilder is used in order to be version independent
|
//SafeCacheBuilder is used in order to be version independent
|
||||||
private final ConcurrentMap<String, PlayerSession> session = SafeCacheBuilder.<String, PlayerSession>newBuilder()
|
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)
|
//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>() {
|
.build(new CacheLoader<String, PlayerSession>() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -52,6 +57,7 @@ public class FastLogin extends JavaPlugin {
|
|||||||
public void onLoad() {
|
public void onLoad() {
|
||||||
//online mode is only changeable after a restart so check it here
|
//online mode is only changeable after a restart so check it here
|
||||||
if (getServer().getOnlineMode()) {
|
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");
|
getLogger().severe("Server have to be in offline mode");
|
||||||
|
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
@ -69,8 +75,8 @@ public class FastLogin extends JavaPlugin {
|
|||||||
protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager));
|
protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager));
|
||||||
protocolManager.addPacketListener(new StartPacketListener(this, protocolManager));
|
protocolManager.addPacketListener(new StartPacketListener(this, protocolManager));
|
||||||
|
|
||||||
//register commands
|
//register commands using a unique name
|
||||||
getCommand("premium").setExecutor(new PremiumCommand(this));
|
getCommand(getName()).setExecutor(new PremiumCommand(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -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
|
* Gets a thread-safe map about players which are connecting to the server are being
|
||||||
* account)
|
* checked to be premium (paid account)
|
||||||
*
|
*
|
||||||
* @return a thread-safe session map
|
* @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
|
* @return the server KeyPair
|
||||||
*/
|
*/
|
||||||
@ -157,7 +164,7 @@ public class FastLogin extends JavaPlugin {
|
|||||||
return false;
|
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);
|
getServer().getPluginManager().registerEvents(new PlayerListener(this, authPluginHook), this);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,11 @@ import org.bukkit.command.CommandExecutor;
|
|||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
import org.bukkit.entity.Player;
|
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 {
|
public class PremiumCommand implements CommandExecutor {
|
||||||
|
|
||||||
private final FastLogin plugin;
|
private final FastLogin plugin;
|
||||||
|
@ -22,16 +22,24 @@ import java.math.BigInteger;
|
|||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.json.simple.JSONObject;
|
import org.json.simple.JSONObject;
|
||||||
import org.json.simple.JSONValue;
|
import org.json.simple.JSONValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* 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:
|
* Receiving packet information:
|
||||||
* http://wiki.vg/Protocol#Encryption_Response
|
* http://wiki.vg/Protocol#Encryption_Response
|
||||||
*
|
*
|
||||||
@ -40,6 +48,7 @@ import org.json.simple.JSONValue;
|
|||||||
*/
|
*/
|
||||||
public class EncryptionPacketListener extends PacketAdapter {
|
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 static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?";
|
||||||
|
|
||||||
private final ProtocolManager protocolManager;
|
private final ProtocolManager protocolManager;
|
||||||
@ -73,18 +82,18 @@ public class EncryptionPacketListener extends PacketAdapter {
|
|||||||
PacketContainer packet = packetEvent.getPacket();
|
PacketContainer packet = packetEvent.getPacket();
|
||||||
Player player = packetEvent.getPlayer();
|
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();
|
String uniqueSessionKey = player.getAddress().toString();
|
||||||
PlayerSession session = plugin.getSessions().get(uniqueSessionKey);
|
PlayerSession session = plugin.getSessions().get(uniqueSessionKey);
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
disconnect(packetEvent, "Invalid request", Level.FINE
|
disconnect(packetEvent, "Invalid request", Level.FINE
|
||||||
, "Player {0} tried to send encryption response"
|
, "Player {0} tried to send encryption response on an invalid connection state"
|
||||||
+ "on an invalid connection state"
|
|
||||||
, player.getAddress());
|
, player.getAddress());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] sharedSecret = packet.getByteArrays().read(0);
|
byte[] sharedSecret = packet.getByteArrays().read(0);
|
||||||
|
//encrypted verify token
|
||||||
byte[] clientVerify = packet.getByteArrays().read(1);
|
byte[] clientVerify = packet.getByteArrays().read(1);
|
||||||
|
|
||||||
PrivateKey privateKey = plugin.getKeyPair().getPrivate();
|
PrivateKey privateKey = plugin.getKeyPair().getPrivate();
|
||||||
@ -123,13 +132,12 @@ public class EncryptionPacketListener extends PacketAdapter {
|
|||||||
|
|
||||||
String username = session.getUsername();
|
String username = session.getUsername();
|
||||||
if (hasJoinedServer(username, serverId)) {
|
if (hasJoinedServer(username, serverId)) {
|
||||||
session.setVerified(true);
|
|
||||||
|
|
||||||
plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username);
|
plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username);
|
||||||
|
|
||||||
|
session.setVerified(true);
|
||||||
receiveFakeStartPacket(username, player);
|
receiveFakeStartPacket(username, player);
|
||||||
} else {
|
} else {
|
||||||
//user tried to fake a authentification
|
//user tried to fake a authentication
|
||||||
disconnect(packetEvent, "Invalid session", Level.FINE
|
disconnect(packetEvent, "Invalid session", Level.FINE
|
||||||
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
|
, "Player {0} ({1}) tried to log in with an invalid session ServerId: {2}"
|
||||||
, session.getUsername(), player.getAddress(), serverId);
|
, session.getUsername(), player.getAddress(), serverId);
|
||||||
@ -161,6 +169,7 @@ public class EncryptionPacketListener extends PacketAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//try to get the networkManager from ProtocolLib
|
||||||
private Object getNetworkManager(Player player)
|
private Object getNetworkManager(Player player)
|
||||||
throws SecurityException, IllegalAccessException, NoSuchFieldException {
|
throws SecurityException, IllegalAccessException, NoSuchFieldException {
|
||||||
Object injector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
|
Object injector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
|
||||||
@ -184,9 +193,14 @@ public class EncryptionPacketListener extends PacketAdapter {
|
|||||||
String line = reader.readLine();
|
String line = reader.readLine();
|
||||||
if (!line.equals("null")) {
|
if (!line.equals("null")) {
|
||||||
//validate parsing
|
//validate parsing
|
||||||
JSONObject object = (JSONObject) JSONValue.parseWithException(line);
|
//http://wiki.vg/Protocol_Encryption#Server
|
||||||
String uuid = (String) object.get("id");
|
JSONObject userData = (JSONObject) JSONValue.parseWithException(line);
|
||||||
String name = (String) object.get("name");
|
String uuid = (String) userData.get("id");
|
||||||
|
String name = (String) userData.get("name");
|
||||||
|
|
||||||
|
JSONObject properties = (JSONObject) userData.get("properties");
|
||||||
|
//base64 encoded skin data
|
||||||
|
String encodedSkin = (String) properties.get("value");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -199,12 +213,12 @@ public class EncryptionPacketListener extends PacketAdapter {
|
|||||||
return false;
|
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) {
|
private void receiveFakeStartPacket(String username, Player from) {
|
||||||
//fake a new login packet
|
|
||||||
//see StartPacketListener for packet information
|
//see StartPacketListener for packet information
|
||||||
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START, true);
|
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START, true);
|
||||||
|
|
||||||
WrappedGameProfile fakeProfile = WrappedGameProfile.fromOfflinePlayer(Bukkit.getOfflinePlayer(username));
|
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);
|
||||||
startPacket.getGameProfiles().write(0, fakeProfile);
|
startPacket.getGameProfiles().write(0, fakeProfile);
|
||||||
try {
|
try {
|
||||||
//we don't want to handle our own packets so ignore filters
|
//we don't want to handle our own packets so ignore filters
|
||||||
|
@ -12,6 +12,11 @@ import org.bukkit.event.EventHandler;
|
|||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.event.player.PlayerJoinEvent;
|
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 {
|
public class PlayerListener implements Listener {
|
||||||
|
|
||||||
private final FastLogin plugin;
|
private final FastLogin plugin;
|
||||||
@ -30,15 +35,7 @@ public class PlayerListener implements Listener {
|
|||||||
//removing the session because we now use it
|
//removing the session because we now use it
|
||||||
PlayerSession session = plugin.getSessions().remove(address);
|
PlayerSession session = plugin.getSessions().remove(address);
|
||||||
//check if it's the same player as we checked before
|
//check if it's the same player as we checked before
|
||||||
if (session != null && session.getUsername().equals(player.getName())
|
if (session != null && player.getName().equals(session.getUsername()) && session.isVerified()) {
|
||||||
&& session.isVerified()) {
|
|
||||||
//java 8
|
|
||||||
// Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
|
||||||
// if (player.isOnline()) {
|
|
||||||
// plugin.getLogger().log(Level.FINER, "Logging player {0} in", player.getName());
|
|
||||||
// authPlugin.forceLogin(player);
|
|
||||||
// }
|
|
||||||
//java 7+
|
|
||||||
Bukkit.getScheduler().runTaskLater(plugin, new Runnable() {
|
Bukkit.getScheduler().runTaskLater(plugin, new Runnable() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -20,6 +20,11 @@ import java.util.regex.Pattern;
|
|||||||
import org.bukkit.entity.Player;
|
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:
|
* Receiving packet information:
|
||||||
* http://wiki.vg/Protocol#Login_Start
|
* http://wiki.vg/Protocol#Login_Start
|
||||||
*
|
*
|
||||||
@ -27,7 +32,7 @@ import org.bukkit.entity.Player;
|
|||||||
*/
|
*/
|
||||||
public class StartPacketListener extends PacketAdapter {
|
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/";
|
private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
|
||||||
//this includes a-zA-Z1-9_
|
//this includes a-zA-Z1-9_
|
||||||
private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
|
private static final String VALID_PLAYERNAME = "^\\w{2,16}$";
|
||||||
@ -42,7 +47,7 @@ public class StartPacketListener extends PacketAdapter {
|
|||||||
private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
|
private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
|
||||||
|
|
||||||
public StartPacketListener(FastLogin plugin, ProtocolManager protocolManger) {
|
public StartPacketListener(FastLogin plugin, ProtocolManager protocolManger) {
|
||||||
//run async in order to not block the server, because we make api calls to Mojang
|
//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());
|
super(params(plugin, PacketType.Login.Client.START).optionAsync());
|
||||||
|
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
@ -65,16 +70,16 @@ public class StartPacketListener extends PacketAdapter {
|
|||||||
PacketContainer packet = packetEvent.getPacket();
|
PacketContainer packet = packetEvent.getPacket();
|
||||||
Player player = packetEvent.getPlayer();
|
Player player = packetEvent.getPlayer();
|
||||||
|
|
||||||
//this includes ip and port. Should be unique for 2 Minutes
|
//this includes ip:port. Should be unique for an incoming login request with a timeout of 2 minutes
|
||||||
String sessionKey = player.getAddress().toString();
|
String sessionKey = player.getAddress().toString();
|
||||||
|
|
||||||
//remove old data every time on a new login in order to keep the session only for one person
|
//remove old data every time on a new login in order to keep the session only for one person
|
||||||
plugin.getSessions().remove(sessionKey);
|
plugin.getSessions().remove(sessionKey);
|
||||||
|
|
||||||
|
//player.getName() won't work at this state
|
||||||
String username = packet.getGameProfiles().read(0).getName();
|
String username = packet.getGameProfiles().read(0).getName();
|
||||||
plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting to the server"
|
plugin.getLogger().log(Level.FINER, "Player {0} with {1} connecting to the server"
|
||||||
, new Object[]{sessionKey, username});
|
, new Object[]{sessionKey, username});
|
||||||
//do premium login process
|
|
||||||
if (plugin.getEnabledPremium().contains(username) && isPremium(username)) {
|
if (plugin.getEnabledPremium().contains(username) && isPremium(username)) {
|
||||||
//minecraft server implementation
|
//minecraft server implementation
|
||||||
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
|
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
|
||||||
@ -83,7 +88,7 @@ public class StartPacketListener extends PacketAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPremium(String playerName) {
|
private boolean isPremium(String playerName) {
|
||||||
//check if it's a valid playername and the user activated fast logins
|
//check if it's a valid playername
|
||||||
if (playernameMatcher.matcher(playerName).matches()) {
|
if (playernameMatcher.matcher(playerName).matches()) {
|
||||||
//only make a API call if the name is valid existing mojang account
|
//only make a API call if the name is valid existing mojang account
|
||||||
try {
|
try {
|
||||||
@ -115,6 +120,7 @@ public class StartPacketListener extends PacketAdapter {
|
|||||||
.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN, true);
|
.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN, true);
|
||||||
|
|
||||||
newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getKeyPair().getPublic());
|
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[4];
|
byte[] verifyToken = new byte[4];
|
||||||
random.nextBytes(verifyToken);
|
random.nextBytes(verifyToken);
|
||||||
newPacket.getByteArrays().write(0, verifyToken);
|
newPacket.getByteArrays().write(0, verifyToken);
|
||||||
|
Reference in New Issue
Block a user