Added BungeeCord support

This commit is contained in:
games647
2015-11-13 22:46:38 +01:00
parent 834818bb7a
commit c3f8e59a9a
16 changed files with 527 additions and 78 deletions

View File

@@ -1,13 +1,16 @@
package com.github.games647.fastlogin;
import com.github.games647.fastlogin.listener.PlayerListener;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.utility.SafeCacheBuilder;
import com.github.games647.fastlogin.hooks.AuthPlugin;
import com.github.games647.fastlogin.listener.BukkitJoinListener;
import com.github.games647.fastlogin.listener.BungeeCordListener;
import com.github.games647.fastlogin.listener.EncryptionPacketListener;
import com.github.games647.fastlogin.listener.HandshakePacketListener;
import com.github.games647.fastlogin.listener.StartPacketListener;
import com.google.common.cache.CacheLoader;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Sets;
import com.google.common.reflect.ClassPath;
@@ -20,16 +23,16 @@ import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import org.bukkit.entity.Player;
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.
* This plugin checks if a player has a paid account and if so tries to skip offline mode authentication.
*/
public class FastLogin extends JavaPlugin {
//http connection, read timeout and user agent for a connection to mojang api servers
private static final int TIMEOUT = 10 * 1000;
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
@@ -38,11 +41,14 @@ public class FastLogin extends JavaPlugin {
//we need a thread-safe set because we access it async in the packet listener
private final Set<String> enabledPremium = Sets.newConcurrentHashSet();
//player=fake player created by Protocollib | this mapmaker creates a concurrent map with weak keys
private final ConcurrentMap<Player, Object> bungeeCordUsers = new MapMaker().weakKeys().makeMap();
//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()
//2 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
.expireAfterWrite(2, TimeUnit.MINUTES)
.expireAfterWrite(1, TimeUnit.MINUTES)
//mapped by ip:port -> PlayerSession
.build(new CacheLoader<String, PlayerSession>() {
@@ -72,11 +78,15 @@ public class FastLogin extends JavaPlugin {
//register packet listeners on success
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager));
protocolManager.addPacketListener(new HandshakePacketListener(this));
protocolManager.addPacketListener(new StartPacketListener(this, protocolManager));
protocolManager.addPacketListener(new EncryptionPacketListener(this, protocolManager));
//register commands using a unique name
getCommand("premium").setExecutor(new PremiumCommand(this));
//check for incoming messages from the bungeecord version of this plugin
getServer().getMessenger().registerIncomingPluginChannel(this, this.getName(), new BungeeCordListener(this));
}
@Override
@@ -84,11 +94,12 @@ public class FastLogin extends JavaPlugin {
//clean up
session.clear();
enabledPremium.clear();
bungeeCordUsers.clear();
}
/**
* 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
*/
@@ -97,8 +108,20 @@ public class FastLogin extends JavaPlugin {
}
/**
* Gets the server KeyPair. This is used to encrypt or decrypt traffic between
* the client and server
* Gets a concurrent map with weak keys for all bungeecord users
* which could be detected. It's mapped by a fake instance of player
* created by Protocollib and a non-null raw object.
*
* Represents a similar set collection
*
* @return
*/
public ConcurrentMap<Player, Object> getBungeeCordUsers() {
return bungeeCordUsers;
}
/**
* Gets the server KeyPair. This is used to encrypt or decrypt traffic between the client and server
*
* @return the server KeyPair
*/
@@ -166,7 +189,7 @@ public class FastLogin extends JavaPlugin {
}
//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 BukkitJoinListener(this, authPluginHook), this);
return true;
}
}

View File

@@ -1,5 +1,7 @@
package com.github.games647.fastlogin;
import org.apache.commons.lang.ArrayUtils;
/**
* Represents a client connecting to the server.
*
@@ -15,7 +17,7 @@ public class PlayerSession {
public PlayerSession(String username, String serverId, byte[] verifyToken) {
this.username = username;
this.serverId = serverId;
this.verifyToken = verifyToken;
this.verifyToken = ArrayUtils.clone(verifyToken);
}
/**
@@ -37,7 +39,7 @@ public class PlayerSession {
* @return the verify token from the server
*/
public byte[] getVerifyToken() {
return verifyToken;
return ArrayUtils.clone(verifyToken);
}
/**

View File

@@ -0,0 +1,53 @@
package com.github.games647.fastlogin.listener;
import com.github.games647.fastlogin.FastLogin;
import com.github.games647.fastlogin.PlayerSession;
import com.github.games647.fastlogin.hooks.AuthPlugin;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
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 BukkitJoinListener implements Listener {
private static final long DELAY_LOGIN = 2 * 20L;
protected final FastLogin plugin;
protected final AuthPlugin authPlugin;
public BukkitJoinListener(FastLogin plugin, AuthPlugin authPlugin) {
this.plugin = plugin;
this.authPlugin = authPlugin;
}
@EventHandler(ignoreCancelled = true)
public void onJoin(PlayerJoinEvent joinEvent) {
final Player player = joinEvent.getPlayer();
Bukkit.getScheduler().runTaskLater(plugin, new Runnable() {
@Override
public void run() {
String address = player.getAddress().toString();
//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 (player.isOnline() && session != null
&& player.getName().equals(session.getUsername()) && session.isVerified()) {
plugin.getLogger().log(Level.FINE, "Logging player {0} in", player.getName());
authPlugin.forceLogin(player);
}
}
//Wait before auth plugin and we received a message from BungeeCord initializes the player
}, DELAY_LOGIN);
}
}

View File

@@ -0,0 +1,90 @@
package com.github.games647.fastlogin.listener;
import com.github.games647.fastlogin.FastLogin;
import com.github.games647.fastlogin.PlayerSession;
import com.google.common.base.Charsets;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.logging.Level;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener;
/**
* Responsible for receiving messages from a BungeeCord instance.
*
* This class also receives the plugin message from the bungeecord version of this plugin in order to
* get notified if the connection is in online mode.
*/
public class BungeeCordListener implements PluginMessageListener {
private static final String FILE_NAME = "proxy-whitelist.txt";
private final FastLogin plugin;
//null if whitelist is empty so bungeecord support is disabled
private final UUID proxyId;
public BungeeCordListener(FastLogin plugin) {
this.plugin = plugin;
this.proxyId = loadBungeeCordId();
}
@Override
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
if (!channel.equals(plugin.getName())) {
return;
}
ByteArrayDataInput dataInput = ByteStreams.newDataInput(message);
String subchannel = dataInput.readUTF();
plugin.getLogger().log(Level.FINEST, "Received plugin message for subchannel {0} from {1}"
, new Object[]{subchannel, player});
if ("Checked".equalsIgnoreCase(subchannel)) {
//bungeecord UUID
long mostSignificantBits = dataInput.readLong();
long leastSignificantBits = dataInput.readLong();
UUID sourceId = new UUID(mostSignificantBits, leastSignificantBits);
//fails too if no proxy id is specified in the whitelist file
if (sourceId.equals(proxyId)) {
//make sure the proxy is allowed to transfer data to us
String playerName = dataInput.readUTF();
//check if the player is still online or disconnected
Player checkedPlayer = plugin.getServer().getPlayerExact(playerName);
if (checkedPlayer != null && checkedPlayer.isOnline()) {
PlayerSession playerSession = new PlayerSession(playerName, null, null);
playerSession.setVerified(true);
//put it only if the user doesn't has a session open
//so that the player have to send the bungeecord packet and cannot skip the verification then
plugin.getSessions().putIfAbsent(checkedPlayer.getAddress().toString(), playerSession);
}
}
}
}
public UUID loadBungeeCordId() {
File whitelistFile = new File(plugin.getDataFolder(), FILE_NAME);
//create a new folder if it doesn't exist. Fail silently otherwise
whitelistFile.getParentFile().mkdir();
try {
if (!whitelistFile.exists()) {
whitelistFile.createNewFile();
}
String firstLine = Files.readFirstLine(whitelistFile, Charsets.UTF_8);
if (firstLine != null && !firstLine.isEmpty()) {
return UUID.fromString(firstLine.trim());
}
} catch (IOException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to create file for Proxy whitelist", ex);
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to retrieve proxy Id. Disabling BungeeCord support", ex);
}
return null;
}
}

View File

@@ -147,11 +147,11 @@ public class EncryptionPacketListener extends PacketAdapter {
//try to get the networkManager from ProtocolLib
private Object getNetworkManager(Player player)
throws IllegalAccessException, NoSuchFieldException {
Object injector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
Field injectorField = injector.getClass().getDeclaredField("injector");
Object socketInjector = TemporaryPlayerFactory.getInjectorFromPlayer(player);
Field injectorField = socketInjector.getClass().getDeclaredField("injector");
injectorField.setAccessible(true);
Object rawInjector = injectorField.get(injector);
Object rawInjector = injectorField.get(socketInjector);
injectorField = rawInjector.getClass().getDeclaredField("networkManager");
injectorField.setAccessible(true);
@@ -236,7 +236,7 @@ public class EncryptionPacketListener extends PacketAdapter {
//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
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START, true);
PacketContainer startPacket = protocolManager.createPacket(PacketType.Login.Client.START);
//uuid is ignored by the packet definition
WrappedGameProfile fakeProfile = new WrappedGameProfile(UUID.randomUUID(), username);

View File

@@ -0,0 +1,58 @@
package com.github.games647.fastlogin.listener;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.github.games647.fastlogin.FastLogin;
import java.util.logging.Level;
/**
* Listens to incoming handshake packets.
*
* As BungeeCord sends additional information on the Handshake,
* we can detect it and check so if the player is coming from a
* BungeeCord instance. IpForward has to be activated in the
* BungeeCord config to send these extra information.
*
* Packet information:
* http://wiki.vg/Protocol#Handshake
*
* Int=Protocol version
* String=connecting server address (and additional information from BungeeCord)
* int=server port
* int=next state
*/
public class HandshakePacketListener extends PacketAdapter {
//hides the inherit Plugin plugin field, but we need a more detailed type than just Plugin
private final FastLogin plugin;
public HandshakePacketListener(FastLogin plugin) {
//run async in order to not block the server, because we are making api calls to Mojang
super(params(plugin, PacketType.Handshake.Client.SET_PROTOCOL).optionAsync());
this.plugin = plugin;
}
@Override
public void onPacketReceiving(PacketEvent packetEvent) {
PacketContainer packet = packetEvent.getPacket();
PacketType.Protocol nextProtocol = packet.getProtocols().read(0);
//we don't want to listen for server ping.
if (nextProtocol == PacketType.Protocol.LOGIN) {
//here are the information written separated by a space
String hostname = packet.getStrings().read(0);
//https://hub.spigotmc.org/stash/projects/SPIGOT/repos/spigot/browse/CraftBukkit-Patches/0055-BungeeCord-Support.patch
String[] split = hostname.split("\00");
if (split.length == 3 || split.length == 4) {
plugin.getLogger().log(Level.FINER, "Detected BungeeCord for {0}", hostname);
//object = because there are no concurrent sets with weak keys
plugin.getBungeeCordUsers().put(packetEvent.getPlayer(), new Object());
}
}
}
}

View File

@@ -1,54 +0,0 @@
package com.github.games647.fastlogin.listener;
import com.github.games647.fastlogin.FastLogin;
import com.github.games647.fastlogin.PlayerSession;
import com.github.games647.fastlogin.hooks.AuthPlugin;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
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;
public PlayerListener(FastLogin plugin, AuthPlugin authPlugin) {
this.plugin = plugin;
this.authPlugin = authPlugin;
}
@EventHandler(ignoreCancelled = true)
public void onJoin(PlayerJoinEvent joinEvent) {
final Player player = joinEvent.getPlayer();
String address = player.getAddress().toString();
//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 && 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
}, DELAY_LOGIN);
}
}
}

View File

@@ -68,7 +68,7 @@ public class StartPacketListener extends PacketAdapter {
*/
@Override
public void onPacketReceiving(PacketEvent packetEvent) {
Player player = packetEvent.getPlayer();
final Player player = packetEvent.getPlayer();
//this includes ip:port. Should be unique for an incoming login request with a timeout of 2 minutes
String sessionKey = player.getAddress().toString();
@@ -81,7 +81,8 @@ 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.getEnabledPremium().contains(username) && isPremiumName(username)) {
if (!plugin.getBungeeCordUsers().containsKey(player)
&& plugin.getEnabledPremium().contains(username) && isPremiumName(username)) {
//minecraft server implementation
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
sentEncryptionRequest(sessionKey, username, player, packetEvent);
@@ -117,7 +118,7 @@ public class StartPacketListener extends PacketAdapter {
* key=public server key
* verifyToken=random 4 byte array
*/
PacketContainer newPacket = protocolManager.createPacket(PacketType.Login.Server.ENCRYPTION_BEGIN, true);
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/