Added auto login without commands (Fixes #2)

This commit is contained in:
games647
2016-01-27 17:21:53 +01:00
parent bd46dae086
commit 157b8499a9
11 changed files with 240 additions and 118 deletions

View File

@ -1,9 +1,13 @@
######0.5
* Added autologin - See config
* Added config
* Added isRegistered API method
* Added forceRegister API method
* Fixed CrazyLogin player data restore -> Fixes memory leaks with this plugin
* Fixed premium name check to protocolsupport
######0.4
* Added forward premium skin

View File

@ -37,7 +37,7 @@ public class EncryptionUtil {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(1024);
keyPairGenerator.initialize(1_024);
return keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException nosuchalgorithmexception) {
//Should be existing in every vm

View File

@ -17,13 +17,12 @@ 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.apache.commons.lang.RandomStringUtils;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
@ -33,10 +32,6 @@ import org.bukkit.plugin.java.JavaPlugin;
*/
public class FastLoginBukkit extends JavaPlugin {
//http connection, read timeout and user agent for a connection to mojang api servers
private static final int TIMEOUT = 1 * 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 = EncryptionUtil.generateKeyPair();
@ -62,9 +57,11 @@ public class FastLoginBukkit extends JavaPlugin {
});
private AuthPlugin authPlugin;
private final MojangApiConnector mojangApiConnector = new MojangApiConnector(this);
@Override
public void onEnable() {
saveDefaultConfig();
if (getServer().getOnlineMode() || !registerHooks()) {
//we need to require offline to prevent a session request for a offline player
getLogger().severe("Server have to be in offline mode and have an auth plugin installed");
@ -108,6 +105,10 @@ public class FastLoginBukkit extends JavaPlugin {
}
}
public String generateStringPassword() {
return RandomStringUtils.random(8, true, true);
}
/**
* Gets a thread-safe map about players which are connecting to the server are being checked to be premium (paid
* account)
@ -158,22 +159,13 @@ public class FastLoginBukkit extends JavaPlugin {
}
/**
* Prepares a Mojang API connection. The connection is not started in this method
* Gets the a connection in order to access important
* features from the Mojang API.
*
* @param url the url connecting to
* @return the prepared connection
*
* @throws IOException on invalid url format or on {@link java.net.URL#openConnection() }
* @return the connector instance
*/
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;
public MojangApiConnector getApiConnector() {
return mojangApiConnector;
}
private boolean registerHooks() {

View File

@ -0,0 +1,103 @@
package com.github.games647.fastlogin.bukkit;
import com.comphenix.protocol.wrappers.WrappedSignedProperty;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.logging.Level;
import java.util.regex.Pattern;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
public class MojangApiConnector {
//http connection, read timeout and user agent for a connection to mojang api servers
private static final int TIMEOUT = 1 * 1_000;
private static final String USER_AGENT = "Premium-Checker";
//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?";
//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}$";
//compile the pattern only on plugin enable -> and this have to be threadsafe
private final Pattern playernameMatcher = Pattern.compile(VALID_PLAYERNAME);
private final FastLoginBukkit plugin;
public MojangApiConnector(FastLoginBukkit plugin) {
this.plugin = plugin;
}
public boolean isPremiumName(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 = 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;
}
public boolean hasJoinedServer(PlayerSession session, String serverId) {
try {
String url = HAS_JOINED_URL + "username=" + session.getUsername() + "&serverId=" + serverId;
HttpURLConnection conn = getConnection(url);
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = reader.readLine();
if (line != null && !line.equals("null")) {
//validate parsing
//http://wiki.vg/Protocol_Encryption#Server
JSONObject userData = (JSONObject) JSONValue.parseWithException(line);
String uuid = (String) userData.get("id");
JSONArray properties = (JSONArray) userData.get("properties");
JSONObject skinProperty = (JSONObject) properties.get(0);
String propertyName = (String) skinProperty.get("name");
if (propertyName.equals("textures")) {
String skinValue = (String) skinProperty.get("value");
String signature = (String) skinProperty.get("signature");
session.setSkin(WrappedSignedProperty.fromValues(propertyName, skinValue, signature));
}
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 session", ex);
}
//this connection doesn't need to be closed. So can make use of keep alive in java
return false;
}
private 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;
}
}

View File

@ -17,6 +17,7 @@ public class PlayerSession {
private WrappedSignedProperty skinProperty;
private boolean verified;
private boolean registered;
public PlayerSession(String username, String serverId, byte[] verifyToken) {
this.username = username;
@ -73,6 +74,24 @@ public class PlayerSession {
this.skinProperty = skinProperty;
}
/**
* Sets whether the account of this player already exists
*
* @param registered whether the account exists
*/
public synchronized void setRegistered(boolean registered) {
this.registered = registered;
}
/**
* Gets whether the account of this player already exists.
*
* @return whether the account exists
*/
public synchronized boolean needsRegistration() {
return !registered;
}
/**
* Sets whether the player has a premium (paid account) account
* and valid session

View File

@ -16,6 +16,7 @@ public class AuthMeHook implements AuthPlugin {
NewAPI.getInstance().forceLogin(player);
}
@Override
public boolean isRegistered(String playerName) {
return NewAPI.getInstance().isRegistered(playerName);
}

View File

@ -8,6 +8,7 @@ import com.github.games647.fastlogin.bukkit.PlayerSession;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@ -55,8 +56,19 @@ public class BukkitJoinListener implements Listener {
player.setMetadata(plugin.getName(), new FixedMetadataValue(plugin, true));
//check if it's the same player as we checked before
if (session != null && player.getName().equals(session.getUsername()) && session.isVerified()) {
plugin.getLogger().log(Level.FINE, "Logging player {0} in", player.getName());
plugin.getAuthPlugin().forceLogin(player);
if (session.needsRegistration()) {
plugin.getLogger().log(Level.FINE, "Register player {0}", player.getName());
String generatedPassword = plugin.generateStringPassword();
plugin.getAuthPlugin().forceRegister(player, generatedPassword);
player.sendMessage(ChatColor.DARK_GREEN + "Auto registered with password: "
+ generatedPassword);
player.sendMessage(ChatColor.DARK_GREEN + "You may want change it?");
} else {
plugin.getLogger().log(Level.FINE, "Logging player {0} in", player.getName());
plugin.getAuthPlugin().forceLogin(player);
player.sendMessage(ChatColor.DARK_GREEN + "Auto logged in");
}
}
}
}

View File

@ -9,18 +9,14 @@ 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.comphenix.protocol.wrappers.WrappedSignedProperty;
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.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;
@ -29,9 +25,6 @@ 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.
@ -50,9 +43,6 @@ 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;
//hides the inherit Plugin plugin field, but we need this type
private final FastLoginBukkit plugin;
@ -111,7 +101,7 @@ public class EncryptionPacketListener extends PacketAdapter {
String serverId = (new BigInteger(serverIdHash)).toString(16);
String username = session.getUsername();
if (hasJoinedServer(session, serverId)) {
if (plugin.getApiConnector().hasJoinedServer(session, serverId)) {
plugin.getLogger().log(Level.FINE, "Player {0} has a verified premium account", username);
session.setVerified(true);
@ -203,40 +193,6 @@ public class EncryptionPacketListener extends PacketAdapter {
}
}
private boolean hasJoinedServer(PlayerSession session, String serverId) {
try {
String url = HAS_JOINED_URL + "username=" + session.getUsername() + "&serverId=" + serverId;
HttpURLConnection conn = plugin.getConnection(url);
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = reader.readLine();
if (line != null && !line.equals("null")) {
//validate parsing
//http://wiki.vg/Protocol_Encryption#Server
JSONObject userData = (JSONObject) JSONValue.parseWithException(line);
String uuid = (String) userData.get("id");
JSONArray properties = (JSONArray) userData.get("properties");
JSONObject skinProperty = (JSONObject) properties.get(0);
String propertyName = (String) skinProperty.get("name");
if (propertyName.equals("textures")) {
String skinValue = (String) skinProperty.get("value");
String signature = (String) skinProperty.get("signature");
session.setSkin(WrappedSignedProperty.fromValues(propertyName, skinValue, signature));
}
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 session", 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

View File

@ -8,6 +8,7 @@ import java.net.InetSocketAddress;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@ -15,6 +16,7 @@ import org.bukkit.event.player.PlayerJoinEvent;
import protocolsupport.api.events.PlayerLoginStartEvent;
import protocolsupport.api.events.PlayerPropertiesResolveEvent;
import protocolsupport.api.events.PlayerPropertiesResolveEvent.ProfileProperty;
public class ProtcolSupportListener implements Listener {
@ -34,12 +36,11 @@ public class ProtcolSupportListener implements Listener {
String playerName = loginStartEvent.getName();
if (plugin.getEnabledPremium().contains(playerName)) {
loginStartEvent.setOnlineMode(true);
InetSocketAddress address = loginStartEvent.getAddress();
PlayerSession playerSession = new PlayerSession(playerName, null, null);
plugin.getSessions().put(address.toString(), playerSession);
// loginStartEvent.setUseOnlineModeUUID(true);
//the player have to be registered in order to invoke the command
startPremiumSession(playerName, loginStartEvent, true);
} else if (plugin.getConfig().getBoolean("autologin") && !plugin.getAuthPlugin().isRegistered(playerName)) {
startPremiumSession(playerName, loginStartEvent, false);
plugin.getEnabledPremium().add(playerName);
}
}
@ -50,8 +51,7 @@ public class ProtcolSupportListener implements Listener {
if (session != null) {
session.setVerified(true);
PlayerPropertiesResolveEvent.ProfileProperty skinProperty = propertiesResolveEvent.getProperties()
.get("textures");
ProfileProperty skinProperty = propertiesResolveEvent.getProperties().get("textures");
if (skinProperty != null) {
WrappedSignedProperty signedProperty = WrappedSignedProperty
.fromValues(skinProperty.getName(), skinProperty.getValue(), skinProperty.getSignature());
@ -74,12 +74,35 @@ public class ProtcolSupportListener implements Listener {
if (player.isOnline()) {
//check if it's the same player as we checked before
if (session != null && player.getName().equals(session.getUsername()) && session.isVerified()) {
plugin.getLogger().log(Level.FINE, "Logging player {0} in", player.getName());
plugin.getAuthPlugin().forceLogin(player);
if (session.needsRegistration()) {
plugin.getLogger().log(Level.FINE, "Register player {0}", player.getName());
String generatedPassword = plugin.generateStringPassword();
plugin.getAuthPlugin().forceRegister(player, generatedPassword);
player.sendMessage(ChatColor.DARK_GREEN + "Auto registered with password: "
+ generatedPassword);
player.sendMessage(ChatColor.DARK_GREEN + "You may want change it?");
} else {
plugin.getLogger().log(Level.FINE, "Logging player {0} in", player.getName());
plugin.getAuthPlugin().forceLogin(player);
player.sendMessage(ChatColor.DARK_GREEN + "Auto logged in");
}
}
}
}
//Wait before auth plugin and we received a message from BungeeCord initializes the player
}, DELAY_LOGIN);
}
private void startPremiumSession(String playerName, PlayerLoginStartEvent loginStartEvent, boolean registered) {
if (plugin.getApiConnector().isPremiumName(playerName)) {
loginStartEvent.setOnlineMode(true);
InetSocketAddress address = loginStartEvent.getAddress();
PlayerSession playerSession = new PlayerSession(playerName, null, null);
playerSession.setRegistered(registered);
plugin.getSessions().put(address.toString(), playerSession);
// loginStartEvent.setUseOnlineModeUUID(true);
}
}
}

View File

@ -8,13 +8,10 @@ import com.comphenix.protocol.events.PacketEvent;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.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;
@ -31,10 +28,6 @@ import org.bukkit.entity.Player;
*/
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;
@ -43,8 +36,6 @@ public class StartPacketListener extends PacketAdapter {
//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(FastLoginBukkit plugin, ProtocolManager protocolManger) {
//run async in order to not block the server, because we are making api calls to Mojang
@ -80,35 +71,43 @@ public class StartPacketListener extends PacketAdapter {
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.getBungeeCordUsers().containsKey(player)
&& plugin.getEnabledPremium().contains(username) && isPremiumName(username)) {
if (!plugin.getBungeeCordUsers().containsKey(player)) {
if (plugin.getEnabledPremium().contains(username)) {
enablePremiumLogin(username, sessionKey, player, packetEvent, true);
} else if (plugin.getConfig().getBoolean("autologin") && !plugin.getAuthPlugin().isRegistered(username)) {
enablePremiumLogin(username, sessionKey, player, packetEvent, false);
plugin.getEnabledPremium().add(username);
}
}
}
private void enablePremiumLogin(String username, String sessionKey, Player player, PacketEvent packetEvent
, boolean registered) {
if (plugin.getApiConnector().isPremiumName(username)) {
plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username);
//minecraft server implementation
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
sentEncryptionRequest(sessionKey, username, player, packetEvent);
}
}
private boolean isPremiumName(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();
//randomized server id to make sure the request is for our server
//this could be relevant http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/
String serverId = Long.toString(random.nextLong(), 16);
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);
//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);
boolean success = sentEncryptionRequest(player, serverId, verifyToken);
if (success) {
PlayerSession playerSession = new PlayerSession(username, serverId, verifyToken);
playerSession.setRegistered(registered);
plugin.getSessions().put(sessionKey, playerSession);
//cancel only if the player has a paid account otherwise login as normal offline player
packetEvent.setCancelled(true);
}
//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);
private boolean sentEncryptionRequest(Player player, String serverId, byte[] verifyToken) {
try {
/**
* Packet Information: http://wiki.vg/Protocol#Encryption_Request
@ -119,24 +118,18 @@ public class StartPacketListener extends PacketAdapter {
*/
PacketContainer newPacket = protocolManager.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN);
//randomized server id to make sure the request is for our server
//this could be relevant http://www.sk89q.com/2011/09/minecraft-name-spoofing-exploit/
String serverId = Long.toString(random.nextLong(), 16);
newPacket.getStrings().write(0, serverId);
newPacket.getSpecificModifier(PublicKey.class).write(0, plugin.getServerKey().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);
//serverId is a empty string
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(username, serverId, verifyToken));
packetEvent.setCancelled(true);
return true;
} catch (InvocationTargetException ex) {
plugin.getLogger().log(Level.SEVERE, "Cannot send encryption packet. Falling back to normal login", ex);
}
return false;
}
}

View File

@ -0,0 +1,19 @@
# FastLogin config
# You can access the newest config here:
# https://github.com/games647/FastLogin/blob/master/bukkit/src/main/resources/config.yml
# Request a premium login without forcing the player to type a command
#
# If you activate autologin, this plugin will check/do these points on login:
# 1. An existing cracked account shouldn't exist
# -> paid accounts cannot steal the existing account of cracked players
# - (Already registered players could still use the /premium command to activate premium checks)
# 2. Automatically registers an account with a strong random generated password
# -> cracked player cannot register an account for the premium player and so cannot the steal the account
#
# Furthermore the premium player check have to be made based on the player name
# This means if a cracked player connects to the server and we request a paid account login from this player
# the player just disconnect and sees the message: 'bad login'
# There is no way to change this message
# For more information: https://github.com/games647/FastLogin#why-do-players-have-to-invoke-a-command
autologin: false