Compare commits

...

6 Commits

Author SHA1 Message Date
games647
5676e99dec Require player re-join to confirm auto-login request
Fixes #159
Fixes #242
Fixes #256
Fixes #286
2020-03-20 16:06:42 +01:00
games647
65469ed579 Make scheduler platform dependent 2020-03-20 13:44:58 +01:00
games647
8ecb5657d3 Drop nullable annotation usage to compile
Thanks CI
2020-03-14 19:06:05 +01:00
games647
b0cf6e39c7 Drop forced dependency 2020-03-14 19:01:09 +01:00
games647
88a526b5bf Drop support for Minecraft 1.7 2020-03-14 18:51:27 +01:00
games647
57b84509da Limit the total number of running threads
(Related #304)
2020-03-14 18:20:34 +01:00
29 changed files with 350 additions and 104 deletions

View File

@@ -51,7 +51,7 @@ https://ci.codemc.org/job/Games647/job/FastLogin/changes
* Plugin:
* [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) or
* [ProtocolSupport](https://www.spigotmc.org/resources/protocolsupport.7201/)
* [Spigot](https://www.spigotmc.org) 1.7.10+
* [Spigot](https://www.spigotmc.org) 1.8.8+
* Java 8+
* Run Spigot and/or BungeeCord/Waterfall in offline mode (see server.properties or config.yml)
* An auth plugin. Supported plugins

View File

@@ -30,6 +30,14 @@ public class BukkitLoginSession extends LoginSession {
this.verifyToken = verifyToken.clone();
}
public BukkitLoginSession(String username, String serverId, byte[] verifyToken, StoredProfile profile) {
// confirmation login
super(username, profile);
this.serverId = serverId;
this.verifyToken = verifyToken.clone();
}
//available for BungeeCord
public BukkitLoginSession(String username, boolean registered) {
this(username, "", EMPTY_ARRAY, registered, null);

View File

@@ -0,0 +1,27 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.AsyncScheduler;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadFactory;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.slf4j.Logger;
public class BukkitScheduler extends AsyncScheduler {
private final Plugin plugin;
private final Executor syncExecutor;
public BukkitScheduler(Plugin plugin, Logger logger, ThreadFactory threadFactory) {
super(logger, threadFactory);
this.plugin = plugin;
syncExecutor = r -> Bukkit.getScheduler().runTask(plugin, r);
}
public Executor getSyncExecutor() {
return syncExecutor;
}
}

View File

@@ -41,17 +41,24 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
//1 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
private final ConcurrentMap<String, BukkitLoginSession> loginSession = CommonUtil.buildCache(1, -1);
private final Logger logger = CommonUtil.createLoggerFromJDK(getLogger());
private final Map<UUID, PremiumStatus> premiumPlayers = new ConcurrentHashMap<>();
private final Logger logger;
private boolean serverStarted;
private boolean bungeeCord;
private final BukkitScheduler scheduler;
private FastLoginCore<Player, CommandSender, FastLoginBukkit> core;
public FastLoginBukkit() {
this.logger = CommonUtil.createLoggerFromJDK(getLogger());
this.scheduler = new BukkitScheduler(this, logger, getThreadFactory());
}
@Override
public void onEnable() {
core = new FastLoginCore<>(this);
core.load();
try {
bungeeCord = Class.forName("org.spigotmc.SpigotConfig").getDeclaredField("bungee").getBoolean(null);
} catch (ClassNotFoundException notFoundEx) {
@@ -72,7 +79,7 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
setServerStarted();
// check for incoming messages from the bungeecord version of this plugin
String forceChannel = new NamespaceKey(getName(), LoginActionMessage.FORCE_CHANNEL).getCombinedName();
String forceChannel = NamespaceKey.getCombined(getName(), LoginActionMessage.FORCE_CHANNEL);
getServer().getMessenger().registerIncomingPluginChannel(this, forceChannel, new BungeeListener(this));
// outgoing
@@ -195,6 +202,11 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
return logger;
}
@Override
public BukkitScheduler getScheduler() {
return scheduler;
}
@Override
public void sendMessage(CommandSender receiver, String message) {
receiver.sendMessage(message);

View File

@@ -3,7 +3,6 @@ package com.github.games647.fastlogin.bukkit.command;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.StoredProfile;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@@ -44,7 +43,7 @@ public class CrackedCommand extends ToggleCommand {
profile.setPremium(false);
profile.setId(null);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
});
} else {
@@ -76,7 +75,7 @@ public class CrackedCommand extends ToggleCommand {
plugin.getCore().sendLocaleMessage("remove-premium", sender);
profile.setPremium(false);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
});
}

View File

@@ -1,11 +1,9 @@
package com.github.games647.fastlogin.bukkit.command;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.ConfirmationState;
import com.github.games647.fastlogin.core.StoredProfile;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -37,31 +35,44 @@ public class PremiumCommand extends ToggleCommand {
return;
}
if (forwardPremiumCommand(sender, sender.getName())) {
Player player = (Player) sender;
String playerName = sender.getName();
if (forwardPremiumCommand(sender, playerName)) {
return;
}
UUID id = ((Player) sender).getUniqueId();
if (plugin.getConfig().getBoolean("premium-warning") && !plugin.getCore().getPendingConfirms().contains(id)) {
sender.sendMessage(plugin.getCore().getMessage("premium-warning"));
plugin.getCore().getPendingConfirms().add(id);
return;
// non-bungee mode
if (plugin.getConfig().getBoolean("premium-confirm")) {
ConfirmationState state = plugin.getCore().getPendingConfirms().get(playerName);
if (state == null) {
// no pending confirmation
plugin.getCore().getPendingConfirms().put(playerName, ConfirmationState.REQUIRE_RELOGIN);
player.kickPlayer(plugin.getCore().getMessage("premium-confirm"));
} else if (state == ConfirmationState.REQUIRE_AUTH_PLUGIN_LOGIN) {
// player logged in successful using premium authentication
activate(sender, playerName);
}
} else {
activate(sender, playerName);
}
}
plugin.getCore().getPendingConfirms().remove(id);
//todo: load async
StoredProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
if (profile.isPremium()) {
plugin.getCore().sendLocaleMessage("already-exists", sender);
} else {
//todo: resolve uuid
profile.setPremium(true);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.getCore().getStorage().save(profile);
});
private void activate(CommandSender sender, String playerName) {
plugin.getCore().getPendingConfirms().remove(playerName);
plugin.getCore().sendLocaleMessage("add-premium", sender);
}
//todo: load async
StoredProfile profile = plugin.getCore().getStorage().loadProfile(playerName);
if (profile.isPremium()) {
plugin.getCore().sendLocaleMessage("already-exists", sender);
} else {
//todo: resolve uuid
profile.setPremium(true);
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
});
plugin.getCore().sendLocaleMessage("add-premium", sender);
}
}
private void onPremiumOther(CommandSender sender, Command command, String[] args) {
@@ -85,7 +96,7 @@ public class PremiumCommand extends ToggleCommand {
} else {
//todo: resolve uuid
profile.setPremium(true);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
});

View File

@@ -51,7 +51,6 @@ public class ConnectionListener implements Listener {
Player player = quitEvent.getPlayer();
removeBlacklistStatus(player);
plugin.getCore().getPendingConfirms().remove(player.getUniqueId());
plugin.getPremiumPlayers().remove(player.getUniqueId());
}

View File

@@ -7,11 +7,11 @@ import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.event.BukkitFastLoginPreLoginEvent;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
import java.security.PublicKey;
import java.util.Random;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -81,6 +81,26 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
}
}
@Override
public void requestConfirmationLogin(ProtocolLibLoginSource source, StoredProfile profile, String username) {
try {
source.setOnlineMode();
} catch (Exception ex) {
plugin.getLog().error("Cannot send encryption packet. Falling back to cracked login for: {}", profile, ex);
return;
}
String serverId = source.getServerId();
byte[] verify = source.getVerifyToken();
BukkitLoginSession playerSession = new BukkitLoginSession(username, serverId, verify, profile);
plugin.getLoginSessions().put(player.getAddress().toString(), playerSession);
//cancel only if the player has a paid account otherwise login as normal offline player
synchronized (packetEvent.getAsyncMarker().getProcessingLock()) {
packetEvent.setCancelled(true);
}
}
@Override
public void startCrackedSession(ProtocolLibLoginSource source, StoredProfile profile, String username) {
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);

View File

@@ -11,7 +11,6 @@ import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.security.KeyPair;
import java.security.SecureRandom;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import static com.comphenix.protocol.PacketType.Login.Client.ENCRYPTION_BEGIN;
@@ -67,7 +66,7 @@ public class ProtocolLibListener extends PacketAdapter {
packetEvent.getAsyncMarker().incrementProcessingDelay();
Runnable verifyTask = new VerifyResponseTask(plugin, packetEvent, sender, sharedSecret, keyPair);
Bukkit.getScheduler().runTaskAsynchronously(plugin, verifyTask);
plugin.getScheduler().runAsync(verifyTask);
}
private void onLogin(PacketEvent packetEvent, Player player) {
@@ -85,6 +84,6 @@ public class ProtocolLibListener extends PacketAdapter {
packetEvent.getAsyncMarker().incrementProcessingDelay();
Runnable nameCheckTask = new NameCheckTask(plugin, packetEvent, random, player, username, keyPair.getPublic());
Bukkit.getScheduler().runTaskAsynchronously(plugin, nameCheckTask);
plugin.getScheduler().runAsync(nameCheckTask);
}
}

View File

@@ -29,10 +29,6 @@ public class ProtocolLoginSource implements LoginSource {
return loginStartEvent.getAddress();
}
public PlayerLoginStartEvent getLoginStartEvent() {
return loginStartEvent;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +

View File

@@ -6,11 +6,11 @@ import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.event.BukkitFastLoginPreLoginEvent;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
import java.net.InetSocketAddress;
import java.util.Optional;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -84,9 +84,14 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
BukkitLoginSession playerSession = new BukkitLoginSession(username, registered, profile);
plugin.getLoginSessions().put(source.getAddress().toString(), playerSession);
if (plugin.getConfig().getBoolean("premiumUuid")) {
source.getLoginStartEvent().setOnlineMode(true);
}
}
@Override
public void requestConfirmationLogin(ProtocolLoginSource source, StoredProfile profile, String username) {
source.setOnlineMode();
BukkitLoginSession playerSession = new BukkitLoginSession(username, profile);
plugin.getLoginSessions().put(source.getAddress().toString(), playerSession);
}
@Override

View File

@@ -12,6 +12,10 @@ public class BungeeLoginSession extends LoginSession {
super(username, registered, profile);
}
public BungeeLoginSession(String username, StoredProfile profile) {
super(username, profile);
}
public synchronized void setRegistered(boolean registered) {
this.registered = registered;
}
@@ -22,6 +26,7 @@ public class BungeeLoginSession extends LoginSession {
public synchronized void setAlreadySaved(boolean alreadySaved) {
this.alreadySaved = alreadySaved;
this.confirmationLogin = false;
}
public synchronized boolean isAlreadyLogged() {

View File

@@ -3,6 +3,7 @@ package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.bungee.hook.BungeeAuthHook;
import com.github.games647.fastlogin.bungee.listener.ConnectListener;
import com.github.games647.fastlogin.bungee.listener.PluginMessageListener;
import com.github.games647.fastlogin.core.AsyncScheduler;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.message.ChangePremiumMessage;
import com.github.games647.fastlogin.core.message.ChannelMessage;
@@ -37,11 +38,13 @@ public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSen
private final ConcurrentMap<PendingConnection, BungeeLoginSession> session = new MapMaker().weakKeys().makeMap();
private FastLoginCore<ProxiedPlayer, CommandSender, FastLoginBungee> core;
private AsyncScheduler scheduler;
private Logger logger;
@Override
public void onEnable() {
logger = CommonUtil.createLoggerFromJDK(getLogger());
scheduler = new AsyncScheduler(logger, getThreadFactory());
core = new FastLoginCore<>(this);
core.load();
@@ -54,8 +57,8 @@ public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSen
getProxy().getPluginManager().registerListener(this, new PluginMessageListener(this));
//this is required to listen to incoming messages from the server
getProxy().registerChannel(new NamespaceKey(getName(), ChangePremiumMessage.CHANGE_CHANNEL).getCombinedName());
getProxy().registerChannel(new NamespaceKey(getName(), SuccessMessage.SUCCESS_CHANNEL).getCombinedName());
getProxy().registerChannel(NamespaceKey.getCombined(getName(), ChangePremiumMessage.CHANGE_CHANNEL));
getProxy().registerChannel(NamespaceKey.getCombined(getName(), SuccessMessage.SUCCESS_CHANNEL));
registerHook();
}
@@ -117,10 +120,15 @@ public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSen
@SuppressWarnings("deprecation")
public ThreadFactory getThreadFactory() {
return new ThreadFactoryBuilder()
.setNameFormat(getName() + " Database Pool Thread #%1$d")
.setNameFormat(getName() + " Pool Thread #%1$d")
//Hikari create daemons by default
.setDaemon(true)
.setThreadFactory(new GroupedThreadFactory(this, getName()))
.build();
}
@Override
public AsyncScheduler getScheduler() {
return scheduler;
}
}

View File

@@ -4,13 +4,13 @@ import com.github.games647.craftapi.UUIDAdapter;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.bungee.task.AsyncPremiumCheck;
import com.github.games647.fastlogin.bungee.task.ForceLoginTask;
import com.github.games647.fastlogin.core.ConfirmationState;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
import java.lang.reflect.Field;
import java.util.UUID;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
@@ -48,7 +48,7 @@ public class ConnectListener implements Listener {
PendingConnection connection = preLoginEvent.getConnection();
Runnable asyncPremiumCheck = new AsyncPremiumCheck(plugin, preLoginEvent, connection);
ProxyServer.getInstance().getScheduler().runAsync(plugin, asyncPremiumCheck);
plugin.getScheduler().runAsync(asyncPremiumCheck);
}
@EventHandler(priority = EventPriority.LOWEST)
@@ -101,13 +101,13 @@ public class ConnectListener implements Listener {
Server server = serverConnectedEvent.getServer();
Runnable loginTask = new ForceLoginTask(plugin.getCore(), player, server);
ProxyServer.getInstance().getScheduler().runAsync(plugin, loginTask);
plugin.getScheduler().runAsync(loginTask);
}
@EventHandler
public void onDisconnect(PlayerDisconnectEvent disconnectEvent) {
ProxiedPlayer player = disconnectEvent.getPlayer();
plugin.getSession().remove(player.getPendingConnection());
plugin.getCore().getPendingConfirms().remove(player.getUniqueId());
plugin.getCore().getPendingConfirms().remove(player.getName(), ConfirmationState.REQUIRE_AUTH_PLUGIN_LOGIN);
}
}

View File

@@ -3,6 +3,7 @@ package com.github.games647.fastlogin.bungee.listener;
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.bungee.task.AsyncToggleMessage;
import com.github.games647.fastlogin.core.ConfirmationState;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.message.ChangePremiumMessage;
import com.github.games647.fastlogin.core.message.NamespaceKey;
@@ -14,8 +15,6 @@ import com.google.common.io.ByteStreams;
import java.util.Arrays;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.api.event.PluginMessageEvent;
@@ -56,40 +55,57 @@ public class PluginMessageListener implements Listener {
byte[] data = Arrays.copyOf(pluginMessageEvent.getData(), pluginMessageEvent.getData().length);
ProxiedPlayer forPlayer = (ProxiedPlayer) pluginMessageEvent.getReceiver();
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> readMessage(forPlayer, channel, data));
plugin.getScheduler().runAsync(() -> readMessage(forPlayer, channel, data));
}
private void readMessage(ProxiedPlayer forPlayer, String channel, byte[] data) {
FastLoginCore<ProxiedPlayer, CommandSender, FastLoginBungee> core = plugin.getCore();
private void readMessage(ProxiedPlayer fromPlayer, String channel, byte[] data) {
ByteArrayDataInput dataInput = ByteStreams.newDataInput(data);
if (successChannel.equals(channel)) {
onSuccessMessage(forPlayer);
onSuccessMessage(fromPlayer);
} else if (changeChannel.equals(channel)) {
ChangePremiumMessage changeMessage = new ChangePremiumMessage();
changeMessage.readFrom(dataInput);
String playerName = changeMessage.getPlayerName();
boolean isSourceInvoker = changeMessage.isSourceInvoker();
if (changeMessage.shouldEnable()) {
if (playerName.equals(forPlayer.getName()) && plugin.getCore().getConfig().get("premium-warning", true)
&& !core.getPendingConfirms().contains(forPlayer.getUniqueId())) {
String message = core.getMessage("premium-warning");
forPlayer.sendMessage(TextComponent.fromLegacyText(message));
core.getPendingConfirms().add(forPlayer.getUniqueId());
return;
}
core.getPendingConfirms().remove(forPlayer.getUniqueId());
Runnable task = new AsyncToggleMessage(core, forPlayer, playerName, true, isSourceInvoker);
ProxyServer.getInstance().getScheduler().runAsync(plugin, task);
} else {
Runnable task = new AsyncToggleMessage(core, forPlayer, playerName, false, isSourceInvoker);
ProxyServer.getInstance().getScheduler().runAsync(plugin, task);
}
onChangeMessage(fromPlayer, changeMessage.shouldEnable(), playerName, isSourceInvoker);
}
}
private void onChangeMessage(ProxiedPlayer fromPlayer, boolean shouldEnable, String playerName, boolean isSourceInvoker) {
FastLoginCore<ProxiedPlayer, CommandSender, FastLoginBungee> core = plugin.getCore();
if (shouldEnable) {
if (!isSourceInvoker) {
// fromPlayer is not the target player
activePremiumLogin(fromPlayer, playerName, false);
return;
}
if (plugin.getCore().getConfig().getBoolean("premium-confirm", true)) {
ConfirmationState state = plugin.getCore().getPendingConfirms().get(playerName);
if (state == null) {
// no pending confirmation
core.sendLocaleMessage("premium-confirm", fromPlayer);
core.getPendingConfirms().put(playerName, ConfirmationState.REQUIRE_RELOGIN);
} else if (state == ConfirmationState.REQUIRE_AUTH_PLUGIN_LOGIN) {
// player logged in successful using premium authentication
activePremiumLogin(fromPlayer, playerName, true);
}
} else {
activePremiumLogin(fromPlayer, playerName, true);
}
} else {
Runnable task = new AsyncToggleMessage(core, fromPlayer, playerName, false, isSourceInvoker);
plugin.getScheduler().runAsync(task);
}
}
private void activePremiumLogin(ProxiedPlayer fromPlayer, String playerName, boolean isSourceInvoker) {
plugin.getCore().getPendingConfirms().remove(playerName);
Runnable task = new AsyncToggleMessage(plugin.getCore(), fromPlayer, playerName, true, isSourceInvoker);
plugin.getScheduler().runAsync(task);
}
private void onSuccessMessage(ProxiedPlayer forPlayer) {
if (forPlayer.getPendingConnection().isOnlineMode()) {
//bukkit module successfully received and force logged in the user
@@ -99,9 +115,12 @@ public class PluginMessageListener implements Listener {
loginSession.setRegistered(true);
if (!loginSession.isAlreadySaved()) {
playerProfile.setPremium(true);
plugin.getCore().getStorage().save(playerProfile);
loginSession.setAlreadySaved(true);
playerProfile.setId(loginSession.getUuid());
playerProfile.setPremium(true);
plugin.getCore().getStorage().save(playerProfile);
}
}
}

View File

@@ -60,6 +60,12 @@ public class AsyncPremiumCheck extends JoinManagement<ProxiedPlayer, CommandSend
plugin.getCore().getPendingLogin().put(ip + username, new Object());
}
@Override
public void requestConfirmationLogin(BungeeLoginSource source, StoredProfile profile, String username) {
source.setOnlineMode();
plugin.getSession().put(source.getConnection(), new BungeeLoginSession(username, profile));
}
@Override
public void startCrackedSession(BungeeLoginSource source, StoredProfile profile, String username) {
plugin.getSession().put(source.getConnection(), new BungeeLoginSession(username, false, profile));

View File

@@ -45,13 +45,6 @@
<version>1.7.30</version>
</dependency>
<!--GSON is not at the right position for Minecraft 1.7-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.4</version>
</dependency>
<!-- snakeyaml is present in Bungee, Spigot, Cauldron and so we could use this independent implementation -->
<dependency>
<groupId>net.md-5</groupId>
@@ -69,17 +62,21 @@
<dependency>
<groupId>com.github.games647</groupId>
<artifactId>craftapi</artifactId>
<version>0.3</version>
<version>0.4</version>
</dependency>
<!-- APIs we can use because they are available in all platforms (Spigot, Bungee, Cauldron) -->
<!-- APIs we can use because they are available in all platforms (Spigot, Bungee) -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<!-- The Uranium project (fork of Cauldron) uses 17.0 like Spigot 1.8 as experimental feature -->
<!-- Project url: https://github.com/UraniumMC/Uranium -->
<version>10.0.1</version>
<version>17.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.4</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,69 @@
package com.github.games647.fastlogin.core;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
/**
* This limits the number of threads that are used at maximum. Thread creation can be very heavy for the CPU and
* context switching between threads too. However we need many threads for blocking HTTP and database calls.
* Nevertheless this number can be further limited, because the number of actually working database threads
* is limited by the size of our database pool. The goal is to separate concerns into processing and blocking only
* threads.
*/
public class AsyncScheduler {
private static final int MAX_CAPACITY = 1024;
//todo: single thread for delaying and scheduling tasks
private final Logger logger;
// 30 threads are still too many - the optimal solution is to separate into processing and blocking threads
// where processing threads could only be max number of cores while blocking threads could be minimized using
// non-blocking I/O and a single event executor
private final ExecutorService processingPool;
/*
private final ExecutorService databaseExecutor = new ThreadPoolExecutor(1, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(MAX_CAPACITY));
*/
public AsyncScheduler(Logger logger, ThreadFactory threadFactory) {
this.logger = logger;
processingPool = new ThreadPoolExecutor(6, 32,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(MAX_CAPACITY), threadFactory);
}
/*
public <R> CompletableFuture<R> runDatabaseTask(Supplier<R> databaseTask) {
return CompletableFuture.supplyAsync(databaseTask, databaseExecutor)
.exceptionally(error -> {
logger.warn("Error occurred on thread pool", error);
return null;
})
// change context to the processing pool
.thenApplyAsync(r -> r, processingPool);
}
*/
public CompletableFuture<Void> runAsync(Runnable task) {
return CompletableFuture.runAsync(task, processingPool).exceptionally(error -> {
logger.warn("Error occurred on thread pool", error);
return null;
});
}
public void shutdown() {
MoreExecutors.shutdownAndAwaitTermination(processingPool, 1, TimeUnit.MINUTES);
//MoreExecutors.shutdownAndAwaitTermination(databaseExecutor, 1, TimeUnit.MINUTES);
}
}

View File

@@ -72,6 +72,9 @@ public class AuthStorage {
}
public void createTables() throws SQLException {
// choose surrogate PK(ID), because UUID can be null for offline players
// if UUID is always Premium UUID we would have to update offline player entries on insert
// name cannot be PK, because it can be changed for premium players
String createDataStmt = "CREATE TABLE IF NOT EXISTS `" + PREMIUM_TABLE + "` ("
+ "`UserID` INTEGER PRIMARY KEY AUTO_INCREMENT, "
+ "`UUID` CHAR(36), "
@@ -87,7 +90,7 @@ public class AuthStorage {
createDataStmt = createDataStmt.replace("AUTO_INCREMENT", "AUTOINCREMENT");
}
//todo: add uuid index usage
//todo: add unique uuid index usage
try (Connection con = dataSource.getConnection();
Statement createStmt = con.createStatement()) {
createStmt.executeUpdate(createDataStmt);

View File

@@ -0,0 +1,15 @@
package com.github.games647.fastlogin.core;
public enum ConfirmationState {
/**
* Require server login where we request onlinemode authentication
*/
REQUIRE_RELOGIN,
/**
* The command have to be invoked again to confirm that the player who joined through onlinemode knows
* the password of the cracked account
*/
REQUIRE_AUTH_PLUGIN_LOGIN
}

View File

@@ -6,8 +6,6 @@ import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import javax.annotation.Nullable;
public class StoredProfile extends Profile {
private long rowId;
@@ -45,7 +43,7 @@ public class StoredProfile extends Profile {
this.rowId = generatedId;
}
@Nullable
// can be null
public synchronized UUID getId() {
return id;
}

View File

@@ -29,6 +29,9 @@ public class ChangePremiumMessage implements ChannelMessage {
return willEnable;
}
/**
* @return true if the player invoker = target
*/
public boolean isSourceInvoker() {
return isSourceInvoker;
}

View File

@@ -4,6 +4,7 @@ import com.github.games647.craftapi.resolver.MojangResolver;
import com.github.games647.craftapi.resolver.http.RotatingProxySelector;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.ConfirmationState;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.github.games647.fastlogin.core.hooks.DefaultPasswordGenerator;
import com.github.games647.fastlogin.core.hooks.PasswordGenerator;
@@ -24,7 +25,6 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -47,7 +47,7 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
private final Map<String, String> localeMessages = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Object> pendingLogin = CommonUtil.buildCache(5, -1);
private final Collection<UUID> pendingConfirms = new HashSet<>();
private final ConcurrentMap<String, ConfirmationState> pendingConfirms = CommonUtil.buildCache(1, 1024);
private final T plugin;
private final MojangResolver resolver = new MojangResolver();
@@ -204,11 +204,18 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
this.passwordGenerator = passwordGenerator;
}
/**
* @return list of player names that are currently during the login process and might fail and so could be used
* for second attempt logins
*/
public ConcurrentMap<String, Object> getPendingLogin() {
return pendingLogin;
}
public Collection<UUID> getPendingConfirms() {
/**
* @return list of player names that request onlinemode authentication but are not yet approved
*/
public ConcurrentMap<String, ConfirmationState> getPendingConfirms() {
return pendingConfirms;
}
@@ -240,6 +247,9 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
}
public void close() {
plugin.getLog().info("Safely shutting down scheduler. This could take up to one minute.");
plugin.getScheduler().shutdown();
if (storage != null) {
storage.close();
}

View File

@@ -28,10 +28,16 @@ public abstract class ForceLoginManagement<P extends C, C, L extends LoginSessio
StoredProfile playerProfile = session.getProfile();
try {
if (isOnlineMode()) {
if (session.isConfirmationPending()) {
// do not perform force actions, because this confirmation login we have to verify that it's the
// owner of the account
return;
}
//premium player
AuthPlugin<P> authPlugin = core.getAuthPluginHook();
if (authPlugin == null) {
//maybe only bungeecord plugin
//maybe only bungeecord plugin without any auth plugins on Bungee
onForceActionSuccess(session);
} else {
boolean success = true;

View File

@@ -2,6 +2,7 @@ package com.github.games647.fastlogin.core.shared;
import com.github.games647.craftapi.model.Profile;
import com.github.games647.craftapi.resolver.RateLimitException;
import com.github.games647.fastlogin.core.ConfirmationState;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
@@ -37,7 +38,14 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
if (profile.isPremium()) {
requestPremiumLogin(source, profile, username, true);
} else {
startCrackedSession(source, profile, username);
ConfirmationState confirmationState = core.getPendingConfirms().get(username);
if (confirmationState == ConfirmationState.REQUIRE_RELOGIN) {
core.getPendingConfirms().put(username, ConfirmationState.REQUIRE_AUTH_PLUGIN_LOGIN);
requestPremiumLogin(source, profile, username, true);
} else {
// cracked player, but wants to change to premium
startCrackedSession(source, profile, username);
}
}
} else {
if (core.getPendingLogin().remove(ip + username) != null && config.get("secondAttemptCracked", false)) {
@@ -108,5 +116,7 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
public abstract void requestPremiumLogin(S source, StoredProfile profile, String username, boolean registered);
public abstract void requestConfirmationLogin(S source, StoredProfile profile, String username);
public abstract void startCrackedSession(S source, StoredProfile profile, String username);
}

View File

@@ -13,12 +13,19 @@ public abstract class LoginSession {
protected boolean registered;
protected boolean confirmationLogin;
public LoginSession(String username, boolean registered, StoredProfile profile) {
this.username = username;
this.registered = registered;
this.profile = profile;
}
public LoginSession(String username, StoredProfile profile) {
this(username, true, profile);
this.confirmationLogin = true;
}
public String getUsername() {
return username;
}
@@ -54,6 +61,10 @@ public abstract class LoginSession {
this.uuid = uuid;
}
public synchronized boolean isConfirmationPending() {
return confirmationLogin;
}
@Override
public synchronized String toString() {
return this.getClass().getSimpleName() + '{' +

View File

@@ -1,5 +1,8 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.AsyncScheduler;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.nio.file.Path;
import java.util.concurrent.ThreadFactory;
@@ -15,6 +18,8 @@ public interface PlatformPlugin<C> {
void sendMessage(C receiver, String message);
AsyncScheduler getScheduler();
default void sendMultiLineMessage(C receiver, String message) {
for (String line : message.split("%nl%")) {
sendMessage(receiver, line);
@@ -22,6 +27,11 @@ public interface PlatformPlugin<C> {
}
default ThreadFactory getThreadFactory() {
return null;
return new ThreadFactoryBuilder()
.setNameFormat(getName() + " Pool Thread #%1$d")
// Hikari create daemons by default and we could daemon threads for our own scheduler too
// because we safely shutdown
.setDaemon(true)
.build();
}
}

View File

@@ -115,11 +115,11 @@ nameChangeCheck: false
# ChangeSkin, SkinRestorer, ...
forwardSkin: true
# Displays a warning message that this message SHOULD only be invoked by
# users who actually are the owner of this account. So not by cracked players
#
# If they still want to invoke the command, they have to invoke /premium again
premium-warning: true
# Players have to rejoin to verify that they can join through onlinemode authentication.
# After that they have to confirm again using the /premium command that they are also the owner of the auth plugin
# account.
# If the onlinemode authentication fails or the player waits to long, it will fallback to offlinemode authentication.
premium-confirm: true
# If you have autoRegister or nameChangeCheck enabled, you could be rate-limited by Mojang.
# The requests of the both options will be only made by FastLogin if the username is unknown to the server

View File

@@ -89,9 +89,9 @@ invalid-requst: '&4Invalid request'
# Message if the Bukkit isn't fully started to inject the packets
not-started: '&cServer is not fully started yet. Please retry'
# Warning message if a user invoked /premium command
premium-warning: '&c&lWARNING: &6This command should &lonly&6 be invoked if you are the owner of this paid Minecraft account
Type &a/premium&6 again to confirm'
# Premium confirmation message if the player tries to activate the command
premium-confirm: '&6Please relogin and type the command again to apply the changes.
If the request fails or you wait to long, it will fallback to offlinemode.'
# ========= Bungee/Waterfall only ================================