Attempts at mocking the server, fixes

This commit is contained in:
Norbi Peti 2020-09-11 01:20:12 +02:00
parent d784d8b1e2
commit 666f05ff12
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
10 changed files with 111 additions and 98 deletions

40
pom.xml
View file

@ -21,21 +21,8 @@
<testSourceDirectory>target/generated-test-sources/delombok</testSourceDirectory> --> <testSourceDirectory>target/generated-test-sources/delombok</testSourceDirectory> -->
<sourceDirectory>src/main/java</sourceDirectory> <sourceDirectory>src/main/java</sourceDirectory>
<resources> <resources>
<resource>
<directory>src</directory>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
<resource> <resource>
<directory>src/main/resources</directory> <directory>src/main/resources</directory>
<includes>
<include>*.properties</include>
<include>*.yml</include>
<include>*.csv</include>
<include>*.txt</include>
</includes>
<filtering>true</filtering>
</resource> </resource>
</resources> </resources>
<finalName>Chroma-Discord</finalName> <finalName>Chroma-Discord</finalName>
@ -68,28 +55,6 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>copy</id>
<phase>compile</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>target</outputDirectory>
<resources>
<resource>
<directory>resources</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<!-- <plugin> <groupId>org.projectlombok</groupId> <artifactId>lombok-maven-plugin</artifactId> <!-- <plugin> <groupId>org.projectlombok</groupId> <artifactId>lombok-maven-plugin</artifactId>
<version>1.16.16.0</version> <executions> <execution> <id>delombok</id> <phase>generate-sources</phase> <version>1.16.16.0</version> <executions> <execution> <id>delombok</id> <phase>generate-sources</phase>
<goals> <goal>delombok</goal> </goals> <configuration> <addOutputDirectory>false</addOutputDirectory> <goals> <goal>delombok</goal> </goals> <configuration> <addOutputDirectory>false</addOutputDirectory>
@ -241,6 +206,11 @@
<artifactId>mockito-core</artifactId> <artifactId>mockito-core</artifactId>
<version>3.0.0</version> <version>3.0.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.5.10</version>
</dependency>
</dependencies> </dependencies>
<profiles> <profiles>

View file

@ -7,7 +7,6 @@ import buttondevteam.discordplugin.exceptions.ExceptionListenerModule;
import buttondevteam.discordplugin.fun.FunModule; import buttondevteam.discordplugin.fun.FunModule;
import buttondevteam.discordplugin.listeners.CommonListeners; import buttondevteam.discordplugin.listeners.CommonListeners;
import buttondevteam.discordplugin.listeners.MCListener; import buttondevteam.discordplugin.listeners.MCListener;
import buttondevteam.discordplugin.mcchat.MCChatPrivate;
import buttondevteam.discordplugin.mcchat.MCChatUtils; import buttondevteam.discordplugin.mcchat.MCChatUtils;
import buttondevteam.discordplugin.mcchat.MinecraftChatModule; import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
import buttondevteam.discordplugin.mccommands.DiscordMCCommand; import buttondevteam.discordplugin.mccommands.DiscordMCCommand;
@ -260,7 +259,6 @@ public class DiscordPlugin extends ButtonPlugin {
public void pluginDisable() { public void pluginDisable() {
Timings timings = new Timings(); Timings timings = new Timings();
timings.printElapsed("Actual disable start (logout)"); timings.printElapsed("Actual disable start (logout)");
MCChatPrivate.logoutAll();
timings.printElapsed("Config setup"); timings.printElapsed("Config setup");
getConfig().set("serverup", false); getConfig().set("serverup", false);
if (ChromaBot.getInstance() == null) return; //Failed to load if (ChromaBot.getInstance() == null) return; //Failed to load

View file

@ -3,24 +3,17 @@ package buttondevteam.discordplugin.broadcaster;
import buttondevteam.discordplugin.mcchat.MCChatUtils; import buttondevteam.discordplugin.mcchat.MCChatUtils;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import lombok.val; import lombok.val;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.matcher.ElementMatchers;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import java.io.File;
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public class PlayerListWatcher { public class PlayerListWatcher {
@ -28,45 +21,6 @@ public class PlayerListWatcher {
private static Object mock; private static Object mock;
private static MethodHandle fHandle; //Handle for PlayerList.f(EntityPlayer) - Only needed for 1.16 private static MethodHandle fHandle; //Handle for PlayerList.f(EntityPlayer) - Only needed for 1.16
/*public PlayerListWatcher(DedicatedServer minecraftserver) {
super(minecraftserver); // <-- Does some init stuff and calls Bukkit.setServer() so we have to use Objenesis
}
public void sendAll(Packet<?> packet) {
plist.sendAll(packet);
try { // Some messages get sent by directly constructing a packet
if (packet instanceof PacketPlayOutChat) {
Field msgf = PacketPlayOutChat.class.getDeclaredField("a");
msgf.setAccessible(true);
MCChatUtils.forAllMCChat(MCChatUtils.send(((IChatBaseComponent) msgf.get(packet)).toPlainText()));
}
} catch (Exception e) {
TBMCCoreAPI.SendException("Failed to broadcast message sent to all players - hacking failed.", e);
}
}
@Override
public void sendMessage(IChatBaseComponent ichatbasecomponent, boolean flag) { // Needed so it calls the overridden method
plist.getServer().sendMessage(ichatbasecomponent);
ChatMessageType chatmessagetype = flag ? ChatMessageType.SYSTEM : ChatMessageType.CHAT;
// CraftBukkit start - we run this through our processor first so we can get web links etc
this.sendAll(new PacketPlayOutChat(CraftChatMessage.fixComponent(ichatbasecomponent), chatmessagetype));
// CraftBukkit end
}
@Override
public void sendMessage(IChatBaseComponent ichatbasecomponent) { // Needed so it calls the overriden method
this.sendMessage(ichatbasecomponent, true);
}
@Override
public void sendMessage(IChatBaseComponent[] iChatBaseComponents) { // Needed so it calls the overridden method
for (IChatBaseComponent component : iChatBaseComponents) {
sendMessage(component, true);
}
}*/
static boolean hookUpDown(boolean up) throws Exception { static boolean hookUpDown(boolean up) throws Exception {
val csc = Bukkit.getServer().getClass(); val csc = Bukkit.getServer().getClass();
Field conf = csc.getDeclaredField("console"); Field conf = csc.getDeclaredField("console");

View file

@ -186,6 +186,7 @@ public class MCChatListener implements Listener {
* @param wait Wait 5 seconds for the threads to stop * @param wait Wait 5 seconds for the threads to stop
*/ */
public static void stop(boolean wait) { public static void stop(boolean wait) {
MCChatPrivate.logoutAll();
if (sendthread != null) sendthread.interrupt(); if (sendthread != null) sendthread.interrupt();
if (recthread != null) recthread.interrupt(); if (recthread != null) recthread.interrupt();
try { try {
@ -203,7 +204,6 @@ public class MCChatListener implements Listener {
MCChatPrivate.lastmsgPerUser.clear(); MCChatPrivate.lastmsgPerUser.clear();
MCChatCustom.lastmsgCustom.clear(); MCChatCustom.lastmsgCustom.clear();
MCChatUtils.lastmsgfromd.clear(); MCChatUtils.lastmsgfromd.clear();
MCChatUtils.ConnectedSenders.clear();
MCChatUtils.UnconnectedSenders.clear(); MCChatUtils.UnconnectedSenders.clear();
recthread = sendthread = null; recthread = sendthread = null;
} catch (InterruptedException e) { } catch (InterruptedException e) {

View file

@ -28,11 +28,13 @@ public class MCChatPrivate {
if (start) { if (start) {
val sender = DiscordConnectedPlayer.create(user, channel, mcp.getUUID(), op.getName(), mcm); val sender = DiscordConnectedPlayer.create(user, channel, mcp.getUUID(), op.getName(), mcm);
MCChatUtils.addSender(MCChatUtils.ConnectedSenders, user, sender); MCChatUtils.addSender(MCChatUtils.ConnectedSenders, user, sender);
MCChatUtils.LoggedInPlayers.put(mcp.getUUID(), sender);
if (p == null) // Player is offline - If the player is online, that takes precedence if (p == null) // Player is offline - If the player is online, that takes precedence
MCChatUtils.callLoginEvents(sender); MCChatUtils.callLoginEvents(sender);
} else { } else {
val sender = MCChatUtils.removeSender(MCChatUtils.ConnectedSenders, channel.getId(), user); val sender = MCChatUtils.removeSender(MCChatUtils.ConnectedSenders, channel.getId(), user);
assert sender != null; assert sender != null;
MCChatUtils.LoggedInPlayers.remove(sender.getUniqueId());
if (p == null // Player is offline - If the player is online, that takes precedence if (p == null // Player is offline - If the player is online, that takes precedence
&& sender.isLoggedIn()) //Don't call the quit event if login failed && sender.isLoggedIn()) //Don't call the quit event if login failed
MCChatUtils.callLogoutEvent(sender, true); MCChatUtils.callLogoutEvent(sender, true);

View file

@ -28,10 +28,7 @@ import reactor.core.publisher.Mono;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.Arrays; import java.util.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -49,8 +46,8 @@ public class MCChatUtils {
* May contain P&lt;DiscordID&gt; as key for public chat * May contain P&lt;DiscordID&gt; as key for public chat
*/ */
public static final HashMap<String, HashMap<Snowflake, DiscordPlayerSender>> OnlineSenders = new HashMap<>(); public static final HashMap<String, HashMap<Snowflake, DiscordPlayerSender>> OnlineSenders = new HashMap<>();
static @Nullable public static final HashMap<UUID, DiscordConnectedPlayer> LoggedInPlayers = new HashMap<>();
LastMsgData lastmsgdata; static @Nullable LastMsgData lastmsgdata;
static LongObjectHashMap<Message> lastmsgfromd = new LongObjectHashMap<>(); // Last message sent by a Discord user, used for clearing checkmarks static LongObjectHashMap<Message> lastmsgfromd = new LongObjectHashMap<>(); // Last message sent by a Discord user, used for clearing checkmarks
private static MinecraftChatModule module; private static MinecraftChatModule module;
private static final HashMap<Class<? extends Event>, HashSet<String>> staticExcludedPlugins = new HashMap<>(); private static final HashMap<Class<? extends Event>, HashSet<String>> staticExcludedPlugins = new HashMap<>();
@ -283,7 +280,6 @@ public class MCChatUtils {
* @param only Flips the operation and <b>includes</b> the listed plugins * @param only Flips the operation and <b>includes</b> the listed plugins
* @param plugins The plugins to exclude. Not case sensitive. * @param plugins The plugins to exclude. Not case sensitive.
*/ */
@SuppressWarnings("WeakerAccess")
public static void callEventExcluding(Event event, boolean only, String... plugins) { // Copied from Spigot-API and modified a bit public static void callEventExcluding(Event event, boolean only, String... plugins) { // Copied from Spigot-API and modified a bit
if (event.isAsynchronous()) { if (event.isAsynchronous()) {
if (Thread.holdsLock(Bukkit.getPluginManager())) { if (Thread.holdsLock(Bukkit.getPluginManager())) {

View file

@ -40,9 +40,9 @@ class MCListener implements Listener {
return; return;
if (e.getPlayer() instanceof DiscordConnectedPlayer) if (e.getPlayer() instanceof DiscordConnectedPlayer)
return; return;
MCChatUtils.ConnectedSenders.values().stream().flatMap(v -> v.values().stream()) //Only private mcchat should be in ConnectedSenders var dcp = MCChatUtils.LoggedInPlayers.get(e.getPlayer().getUniqueId());
.filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny() if (dcp != null)
.ifPresent(dcp -> MCChatUtils.callLogoutEvent(dcp, false)); MCChatUtils.callLogoutEvent(dcp, false);
} }
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.MONITOR)
@ -75,9 +75,7 @@ class MCListener implements Listener {
MCChatUtils.OnlineSenders.entrySet() MCChatUtils.OnlineSenders.entrySet()
.removeIf(entry -> entry.getValue().entrySet().stream().anyMatch(p -> p.getValue().getUniqueId().equals(e.getPlayer().getUniqueId()))); .removeIf(entry -> entry.getValue().entrySet().stream().anyMatch(p -> p.getValue().getUniqueId().equals(e.getPlayer().getUniqueId())));
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin,
() -> MCChatUtils.ConnectedSenders.values().stream().flatMap(v -> v.values().stream()) () -> Optional.ofNullable(MCChatUtils.LoggedInPlayers.get(e.getPlayer().getUniqueId())).ifPresent(MCChatUtils::callLoginEvents));
.filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny()
.ifPresent(MCChatUtils::callLoginEvents));
Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin,
ChromaBot.getInstance()::updatePlayerList, 5); ChromaBot.getInstance()::updatePlayerList, 5);
final String message = e.getQuitMessage(); final String message = e.getQuitMessage();

View file

@ -5,6 +5,7 @@ import buttondevteam.core.component.channel.Channel;
import buttondevteam.discordplugin.DPUtils; import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordConnectedPlayer; import buttondevteam.discordplugin.DiscordConnectedPlayer;
import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.playerfaker.ServerWatcher;
import buttondevteam.discordplugin.playerfaker.perm.LPInjector; import buttondevteam.discordplugin.playerfaker.perm.LPInjector;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.TBMCSystemChatEvent; import buttondevteam.lib.TBMCSystemChatEvent;
@ -29,6 +30,7 @@ import java.util.stream.Collectors;
*/ */
public class MinecraftChatModule extends Component<DiscordPlugin> { public class MinecraftChatModule extends Component<DiscordPlugin> {
private @Getter MCChatListener listener; private @Getter MCChatListener listener;
private ServerWatcher serverWatcher;
/** /**
* A list of commands that can be used in public chats - Warning: Some plugins will treat players as OPs, always test before allowing a command! * A list of commands that can be used in public chats - Warning: Some plugins will treat players as OPs, always test before allowing a command!
@ -113,6 +115,11 @@ public class MinecraftChatModule extends Component<DiscordPlugin> {
return getConfig().getData("enableVanillaCommands", true); return getConfig().getData("enableVanillaCommands", true);
} }
/**
* Whether players logged on from Discord should be recognised by other plugins. Some plugins might break if it's turned off.
*/
public final ConfigData<Boolean> addFakePlayersToBukkit = getConfig().getData("addFakePlayersToBukkit", true);
@Override @Override
protected void enable() { protected void enable() {
if (DPUtils.disableIfConfigErrorRes(this, chatChannel(), chatChannelMono())) if (DPUtils.disableIfConfigErrorRes(this, chatChannel(), chatChannelMono()))
@ -156,10 +163,22 @@ public class MinecraftChatModule extends Component<DiscordPlugin> {
log("No LuckPerms, not injecting"); log("No LuckPerms, not injecting");
//e.printStackTrace(); //e.printStackTrace();
} }
try { //TODO: Config ^^
serverWatcher = new ServerWatcher();
serverWatcher.enableDisable(true);
} catch (Exception e) {
TBMCCoreAPI.SendException("Failed to hack the server (object)!", e);
}
} }
@Override @Override
protected void disable() { protected void disable() {
try {
serverWatcher.enableDisable(false);
} catch (Exception e) {
TBMCCoreAPI.SendException("Failed to restore the server object!", e);
}
val chcons = MCChatCustom.getCustomChats(); val chcons = MCChatCustom.getCustomChats();
val chconsc = getConfig().getConfig().createSection("chcons"); val chconsc = getConfig().getConfig().createSection("chcons");
for (val chcon : chcons) { for (val chcon : chcons) {

View file

@ -0,0 +1,75 @@
package buttondevteam.discordplugin.playerfaker;
import buttondevteam.discordplugin.mcchat.MCChatUtils;
import lombok.RequiredArgsConstructor;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.mockito.Mockito;
import java.util.*;
public class ServerWatcher {
private List<Player> playerList;
private final List<Player> fakePlayers = new ArrayList<>();
private Server origServer;
public void enableDisable(boolean enable) throws Exception {
var serverField = Bukkit.class.getDeclaredField("server");
serverField.setAccessible(true);
if (enable) {
var serverClass = Bukkit.getServer().getClass();
var mock = Mockito.mock(serverClass, Mockito.withSettings()
.stubOnly().defaultAnswer(invocation -> {
var method = invocation.getMethod();
int pc = method.getParameterCount();
Player player = null;
switch (method.getName()) {
case "getPlayer":
if (pc == 1 && method.getParameterTypes()[0] == UUID.class)
player = MCChatUtils.LoggedInPlayers.get(invocation.<UUID>getArgument(0));
break;
case "getPlayerExact":
if (pc == 1) {
final String argument = invocation.getArgument(0);
player = MCChatUtils.LoggedInPlayers.values().stream()
.filter(dcp -> dcp.getName().equalsIgnoreCase(argument)).findAny().orElse(null);
}
break;
case "getOnlinePlayers":
if (playerList == null) {
@SuppressWarnings("unchecked") var list = (List<Player>) invocation.callRealMethod();
playerList = new AppendListView<>(list, fakePlayers);
}
return playerList;
}
if (player != null)
return player;
return invocation.callRealMethod();
}));
var originalServer = serverField.get(null);
for (var field : serverClass.getFields()) //Copy public fields, private fields aren't accessible directly anyways
field.set(mock, field.get(originalServer));
serverField.set(null, mock);
origServer = (Server) originalServer;
} else if (origServer != null)
serverField.set(null, origServer);
}
@RequiredArgsConstructor
public static class AppendListView<T> extends AbstractSequentialList<T> {
private final List<T> originalList;
private final List<T> additionalList;
@Override
public ListIterator<T> listIterator(int i) {
int os = originalList.size();
return i < os ? originalList.listIterator(i) : additionalList.listIterator(i - os);
}
@Override
public int size() {
return originalList.size() + additionalList.size();
}
}
}

View file

@ -0,0 +1 @@
mock-maker-inline