diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java index b307f4ef..62097309 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java @@ -13,13 +13,11 @@ import com.github.games647.fastlogin.bukkit.listener.EncryptionPacketListener; import com.github.games647.fastlogin.bukkit.listener.ProtocolSupportListener; import com.github.games647.fastlogin.bukkit.listener.StartPacketListener; import com.google.common.cache.CacheLoader; -import com.google.common.collect.Sets; import com.google.common.reflect.ClassPath; import java.io.IOException; import java.security.KeyPair; import java.sql.SQLException; -import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; @@ -46,8 +44,7 @@ public class FastLoginBukkit extends JavaPlugin { //provide a immutable key pair to be thread safe | used for encrypting and decrypting traffic private final KeyPair keyPair = EncryptionUtil.generateKeyPair(); - //we need a thread-safe set because we access it async in the packet listener - private final Set enabledPremium = Sets.newConcurrentHashSet(); + private static final int WORKER_THREADS = 5; private boolean bungeeCord; private Storage storage; @@ -122,8 +119,12 @@ public class FastLoginBukkit extends JavaPlugin { //we are performing HTTP request on these so run it async (seperate from the Netty IO threads) AsynchronousManager asynchronousManager = protocolManager.getAsynchronousManager(); - asynchronousManager.registerAsyncHandler(new StartPacketListener(this, protocolManager)).start(); - asynchronousManager.registerAsyncHandler(new EncryptionPacketListener(this, protocolManager)).start(); + + StartPacketListener startPacketListener = new StartPacketListener(this, protocolManager); + EncryptionPacketListener encryptionPacketListener = new EncryptionPacketListener(this, protocolManager); + + asynchronousManager.registerAsyncHandler(startPacketListener).start(WORKER_THREADS); + asynchronousManager.registerAsyncHandler(encryptionPacketListener).start(WORKER_THREADS); } } @@ -172,13 +173,8 @@ public class FastLoginBukkit extends JavaPlugin { return keyPair; } - /** - * Gets a set of user who activated premium logins - * - * @return user who activated premium logins - */ - public Set getEnabledPremium() { - return enabledPremium; + public Storage getStorage() { + return storage; } /** @@ -231,8 +227,4 @@ public class FastLoginBukkit extends JavaPlugin { authPlugin = authPluginHook; return true; } - - public boolean isBungee() { - return bungeeCord; - } } diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/MojangApiConnector.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/MojangApiConnector.java index 4935b4c9..8eb4c206 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/MojangApiConnector.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/MojangApiConnector.java @@ -38,23 +38,34 @@ public class MojangApiConnector { this.plugin = plugin; } - public boolean isPremiumName(String playerName) { + /** + * + * @param playerName + * @return null on non-premium + */ + public UUID getPremiumUUID(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; + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String line = reader.readLine(); + if (line != null && !line.equals("null")) { + JSONObject userData = (JSONObject) JSONValue.parseWithException(line); + String uuid = (String) userData.get("id"); + return parseId(uuid); + } + } //204 - no content for not found - } catch (IOException ex) { + } catch (Exception 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; + return null; } public boolean hasJoinedServer(PlayerSession session, String serverId) { diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/PlayerProfile.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/PlayerProfile.java index bba5a3ec..f23d0701 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/PlayerProfile.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/PlayerProfile.java @@ -4,11 +4,11 @@ import java.util.UUID; public class PlayerProfile { - private final UUID uuid; private final String playerName; private long userId; + private UUID uuid; private boolean premium; private String lastIp; private long lastLogin; @@ -32,6 +32,10 @@ public class PlayerProfile { this.lastIp = lastIp; } + public String getPlayerName() { + return playerName; + } + public synchronized long getUserId() { return userId; } @@ -40,27 +44,27 @@ public class PlayerProfile { this.userId = generatedId; } - public UUID getUuid() { + public synchronized UUID getUuid() { return uuid; } - public String getPlayerName() { - return playerName; + public synchronized void setUuid(UUID uuid) { + this.uuid = uuid; } - public boolean isPremium() { + public synchronized boolean isPremium() { return premium; } - public void setPremium(boolean premium) { + public synchronized void setPremium(boolean premium) { this.premium = premium; } - public String getLastIp() { + public synchronized String getLastIp() { return lastIp; } - public void setLastIp(String lastIp) { + public synchronized void setLastIp(String lastIp) { this.lastIp = lastIp; } @@ -68,7 +72,7 @@ public class PlayerProfile { return lastLogin; } - public void setLastLogin(long lastLogin) { + public synchronized void setLastLogin(long lastLogin) { this.lastLogin = lastLogin; } } diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/Storage.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/Storage.java index 01f77109..e1aab444 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/Storage.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/Storage.java @@ -65,7 +65,7 @@ public class Storage { Statement statement = con.createStatement(); String createDataStmt = "CREATE TABLE IF NOT EXISTS " + PREMIUM_TABLE + " (" + "`UserID` INTEGER PRIMARY KEY AUTO_INCREMENT, " - + "`UUID` CHAR(36) NOT NULL, " + + "`UUID` CHAR(36), " + "`Name` VARCHAR(16) NOT NULL, " + "`Premium` BOOLEAN NOT NULL, " + "`LastIp` VARCHAR(255) NOT NULL, " @@ -107,6 +107,10 @@ public class Storage { PlayerProfile playerProfile = new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin); profileCache.put(name, playerProfile); return playerProfile; + } else { + PlayerProfile crackedProfile = new PlayerProfile(null, name, false, ""); + profileCache.put(name, crackedProfile); + return crackedProfile; } } catch (SQLException sqlEx) { plugin.getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx); @@ -117,6 +121,40 @@ public class Storage { return null; } +// public PlayerProfile getProfile(UUID uuid, boolean fetch) { +// if (profileCache.containsKey(name)) { +// return profileCache.get(name); +// } else if (fetch) { +// Connection con = null; +// try { +// con = dataSource.getConnection(); +// PreparedStatement loadStatement = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE +// + " WHERE `Name`=? LIMIT 1"); +// loadStatement.setString(1, name); +// +// ResultSet resultSet = loadStatement.executeQuery(); +// if (resultSet.next()) { +// long userId = resultSet.getInt(1); +// UUID uuid = FastLoginBukkit.parseId(resultSet.getString(2)); +//// String name = resultSet.getString(3); +// boolean premium = resultSet.getBoolean(4); +// String lastIp = resultSet.getString(5); +// long lastLogin = resultSet.getTimestamp(6).getTime(); +// PlayerProfile playerProfile = new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin); +// profileCache.put(name, playerProfile); +// return playerProfile; +// } +// +// //todo: result on failure +// } catch (SQLException sqlEx) { +// plugin.getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx); +// } finally { +// closeQuietly(con); +// } +// } +// +// return null; +// } public boolean save(PlayerProfile playerProfile) { Connection con = null; diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/commands/CrackedCommand.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/commands/CrackedCommand.java index c4e6b524..2bf2620c 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/commands/CrackedCommand.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/commands/CrackedCommand.java @@ -1,8 +1,10 @@ package com.github.games647.fastlogin.bukkit.commands; import com.github.games647.fastlogin.bukkit.FastLoginBukkit; +import com.github.games647.fastlogin.bukkit.PlayerProfile; import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; +import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.Command; @@ -20,6 +22,11 @@ public class CrackedCommand implements CommandExecutor { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (plugin.getStorage() == null) { + sender.sendMessage(ChatColor.DARK_RED + "This command is disabled on the backend server"); + return true; + } + if (args.length == 0) { if (!(sender instanceof Player)) { //console or command block @@ -27,10 +34,22 @@ public class CrackedCommand implements CommandExecutor { return true; } - String playerName = sender.getName(); - boolean existed = plugin.getEnabledPremium().remove(playerName); - if (existed) { + Player player = (Player) sender; +// UUID uuid = player.getUniqueId(); + + //todo: load async if it's not in the cache anymore + final PlayerProfile profile = plugin.getStorage().getProfile(player.getName(), false); + if (profile.isPremium()) { sender.sendMessage(ChatColor.DARK_GREEN + "Removed from the list of premium players"); + profile.setPremium(false); + profile.setUuid(null); + Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() { + @Override + public void run() { + plugin.getStorage().save(profile); + } + }); + notifiyBungeeCord((Player) sender); } else { sender.sendMessage(ChatColor.DARK_RED + "You are not in the premium list"); @@ -38,14 +57,16 @@ public class CrackedCommand implements CommandExecutor { return true; } else { - String playerName = args[0]; - boolean existed = plugin.getEnabledPremium().remove(playerName); - if (existed) { - sender.sendMessage(ChatColor.DARK_GREEN + "Removed from the list of premium players"); + sender.sendMessage(ChatColor.DARK_RED + "NOT IMPLEMENTED YET"); + //todo: +// String playerName = args[0]; +// boolean existed = plugin.getEnabledPremium().remove(playerName); +// if (existed) { +// sender.sendMessage(ChatColor.DARK_GREEN + "Removed from the list of premium players"); // notifiyBungeeCord((Player) sender); - } else { - sender.sendMessage(ChatColor.DARK_RED + "User is not in the premium list"); - } +// } else { +// sender.sendMessage(ChatColor.DARK_RED + "User is not in the premium list"); +// } } return true; diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/commands/PremiumCommand.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/commands/PremiumCommand.java index e59b76af..5fc15b4b 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/commands/PremiumCommand.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/commands/PremiumCommand.java @@ -1,6 +1,7 @@ package com.github.games647.fastlogin.bukkit.commands; import com.github.games647.fastlogin.bukkit.FastLoginBukkit; +import com.github.games647.fastlogin.bukkit.PlayerProfile; import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; @@ -25,6 +26,11 @@ public class PremiumCommand implements CommandExecutor { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (plugin.getStorage() == null) { + sender.sendMessage(ChatColor.DARK_RED + "This command is disabled on the backend server"); + return true; + } + if (args.length == 0) { if (!(sender instanceof Player)) { //console or command block @@ -32,24 +38,31 @@ public class PremiumCommand implements CommandExecutor { return true; } - String playerName = sender.getName(); - boolean didntexist = plugin.getEnabledPremium().add(playerName); - if (!didntexist) { + Player player = (Player) sender; +// UUID uuid = player.getUniqueId(); + + //todo: load async if it's not in the cache anymore + PlayerProfile profile = plugin.getStorage().getProfile(player.getName(), false); + if (profile.isPremium()) { sender.sendMessage(ChatColor.DARK_RED + "You are already on the premium list"); } else { + //todo: resolve uuid + profile.setPremium(true); sender.sendMessage(ChatColor.DARK_GREEN + "Added to the list of premium players"); } notifiyBungeeCord((Player) sender); return true; } else { - String playerName = args[0]; - boolean didntexist = plugin.getEnabledPremium().add(playerName); - if (!didntexist) { - sender.sendMessage(ChatColor.DARK_RED + "You are already on the premium list"); - } else { - sender.sendMessage(ChatColor.DARK_GREEN + "Added to the list of premium players"); - } + sender.sendMessage(ChatColor.DARK_RED + "NOT IMPLEMENTED YET"); + //todo: async load +// String playerName = args[0]; +// boolean didntexist = plugin.getEnabledPremium().add(playerName); +// if (!didntexist) { +// sender.sendMessage(ChatColor.DARK_RED + "You are already on the premium list"); +// } else { +// sender.sendMessage(ChatColor.DARK_GREEN + "Added to the list of premium players"); +// } // notifiyBungeeCord(); } diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/BukkitJoinListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/BukkitJoinListener.java index 6b0a4e59..f917e038 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/BukkitJoinListener.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/BukkitJoinListener.java @@ -3,7 +3,9 @@ package com.github.games647.fastlogin.bukkit.listener; import com.comphenix.protocol.wrappers.WrappedGameProfile; import com.comphenix.protocol.wrappers.WrappedSignedProperty; import com.github.games647.fastlogin.bukkit.FastLoginBukkit; +import com.github.games647.fastlogin.bukkit.PlayerProfile; import com.github.games647.fastlogin.bukkit.PlayerSession; +import com.github.games647.fastlogin.bukkit.Storage; import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin; import java.util.logging.Level; @@ -65,7 +67,17 @@ public class BukkitJoinListener implements Listener { if (session.needsRegistration()) { plugin.getLogger().log(Level.FINE, "Register player {0}", player.getName()); - plugin.getEnabledPremium().add(session.getUsername()); + final Storage storage = plugin.getStorage(); + if (storage != null) { + final PlayerProfile playerProfile = storage.getProfile(session.getUsername(), false); + playerProfile.setPremium(true); + Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() { + @Override + public void run() { + storage.save(playerProfile); + } + }); + } String generatedPassword = plugin.generateStringPassword(); authPlugin.forceRegister(player, generatedPassword); diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/ProtocolSupportListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/ProtocolSupportListener.java index 66285533..79bf2aad 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/ProtocolSupportListener.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/ProtocolSupportListener.java @@ -1,10 +1,13 @@ package com.github.games647.fastlogin.bukkit.listener; import com.github.games647.fastlogin.bukkit.FastLoginBukkit; +import com.github.games647.fastlogin.bukkit.PlayerProfile; import com.github.games647.fastlogin.bukkit.PlayerSession; import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin; import java.net.InetSocketAddress; +import java.util.UUID; +import java.util.logging.Level; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -26,19 +29,26 @@ public class ProtocolSupportListener implements Listener { return; } - String playerName = loginStartEvent.getName(); + String username = loginStartEvent.getName(); //remove old data every time on a new login in order to keep the session only for one person - plugin.getSessions().remove(playerName); + plugin.getSessions().remove(username); - BukkitAuthPlugin authPlugin = plugin.getAuthPlugin(); - if (plugin.getEnabledPremium().contains(playerName)) { - //the player have to be registered in order to invoke the command - startPremiumSession(playerName, loginStartEvent, true); - } else if (plugin.getConfig().getBoolean("autoRegister") - && authPlugin != null && !plugin.getAuthPlugin().isRegistered(playerName)) { - startPremiumSession(playerName, loginStartEvent, false); - plugin.getEnabledPremium().add(playerName); + PlayerProfile playerProfile = plugin.getStorage().getProfile(username, true); + if (playerProfile != null) { + //user not exists in the db + if (playerProfile.getUserId() == -1) { + BukkitAuthPlugin authPlugin = plugin.getAuthPlugin(); + if (plugin.getConfig().getBoolean("autoRegister") && !authPlugin.isRegistered(username)) { + UUID premiumUUID = plugin.getApiConnector().getPremiumUUID(username); + if (premiumUUID != null) { + plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username); + startPremiumSession(username, loginStartEvent, false); + } + } + } else if (playerProfile.isPremium()) { + startPremiumSession(username, loginStartEvent, true); + } } } @@ -55,16 +65,14 @@ public class ProtocolSupportListener implements Listener { } private void startPremiumSession(String playerName, PlayerLoginStartEvent loginStartEvent, boolean registered) { - if (plugin.getApiConnector().isPremiumName(playerName)) { - loginStartEvent.setOnlineMode(true); - InetSocketAddress address = loginStartEvent.getAddress(); + loginStartEvent.setOnlineMode(true); + InetSocketAddress address = loginStartEvent.getAddress(); - PlayerSession playerSession = new PlayerSession(playerName, null, null); - playerSession.setRegistered(registered); - plugin.getSessions().put(address.toString(), playerSession); - if (plugin.getConfig().getBoolean("premiumUuid")) { - loginStartEvent.setUseOnlineModeUUID(true); - } + PlayerSession playerSession = new PlayerSession(playerName, null, null); + playerSession.setRegistered(registered); + plugin.getSessions().put(address.toString(), playerSession); + if (plugin.getConfig().getBoolean("premiumUuid")) { + loginStartEvent.setUseOnlineModeUUID(true); } } } diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/StartPacketListener.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/StartPacketListener.java index a3eb5409..63a6c845 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/StartPacketListener.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/StartPacketListener.java @@ -6,12 +6,14 @@ import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.github.games647.fastlogin.bukkit.FastLoginBukkit; +import com.github.games647.fastlogin.bukkit.PlayerProfile; import com.github.games647.fastlogin.bukkit.PlayerSession; import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin; import java.lang.reflect.InvocationTargetException; import java.security.PublicKey; import java.util.Random; +import java.util.UUID; import java.util.logging.Level; import org.bukkit.entity.Player; @@ -72,41 +74,44 @@ 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.isBungee()) { - BukkitAuthPlugin authPlugin = plugin.getAuthPlugin(); - if (plugin.getEnabledPremium().contains(username)) { + + PlayerProfile playerProfile = plugin.getStorage().getProfile(username, true); + if (playerProfile != null) { + //user not exists in the db + if (playerProfile.getUserId() == -1) { + BukkitAuthPlugin authPlugin = plugin.getAuthPlugin(); + if (plugin.getConfig().getBoolean("autoRegister") && !authPlugin.isRegistered(username)) { + UUID premiumUUID = plugin.getApiConnector().getPremiumUUID(username); + if (premiumUUID != null) { + plugin.getLogger().log(Level.FINER, "Player {0} uses a premium username", username); + enablePremiumLogin(username, sessionKey, player, packetEvent, false); + } + } + } else if (playerProfile.isPremium()) { enablePremiumLogin(username, sessionKey, player, packetEvent, true); - } else if (plugin.getConfig().getBoolean("autoRegister") - && authPlugin != null && !plugin.getAuthPlugin().isRegistered(username)) { - enablePremiumLogin(username, sessionKey, player, packetEvent, false); - plugin.getEnabledPremium().add(username); } } } + //minecraft server implementation + //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161 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 + //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); - //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); + //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); - //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); - } + 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); } }