From c8d394c31e86d5b1899d09b0355f320ad32f6ffd Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Wed, 1 Jan 2025 15:37:05 +0100 Subject: [PATCH] Add killswitch feature --- KILLSWITCH.md | 32 +++++ .../java/eu/m724/tweaks/TweaksConfig.java | 13 +- .../java/eu/m724/tweaks/TweaksPlugin.java | 8 +- .../tweaks/killswitch/KillswitchManager.java | 125 ++++++++++++++++++ .../eu/m724/tweaks/killswitch/Ratelimit.java | 26 ++++ src/main/resources/config.yml | 10 ++ src/main/resources/plugin.yml | 6 +- 7 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 KILLSWITCH.md create mode 100644 src/main/java/eu/m724/tweaks/killswitch/KillswitchManager.java create mode 100644 src/main/java/eu/m724/tweaks/killswitch/Ratelimit.java diff --git a/KILLSWITCH.md b/KILLSWITCH.md new file mode 100644 index 0000000..4ac09e3 --- /dev/null +++ b/KILLSWITCH.md @@ -0,0 +1,32 @@ +Killswitch immediately stops the server. + +### Warning +This terminates the server process, meaning it's like you'd pull the power. \ +So you will lose some progress (since the last auto save), or worst case your world (or other data) gets corrupted. + +Terminal froze after kill? `reset` + +### Over a command +No key is required. \ +`/servkill` is the command. Permission: `tweaks724.servkill`. \ +You must grant the permission manually, like with a permission plugin - it's not automatically assigned even to OPs. + +### Over HTTP +HTTP is insecure, meaning others *could* intercept your request to the server and get your key. \ +To encrypt, put this behind a proxy to get HTTPS or a VPN (directly to the server, not a commercial VPN) \ +Or regenerate the key after every usage. + +Make a GET request to `/key/` + +Example: +``` +https://127.0.0.1:57932/key/lNwANMSZhLiTWhNxSoqQ5Q== +|_ server address _| |_ base64 encoded key _| +``` + +The response is a 404 no matter what. Either way, you will notice that the server has stopped. + +The key is generated to `plugins/Tweaks724/storage/killswitch key` \ +To use it with HTTP server, encode it to base64. + +Rate limit is 1 request / 5 minutes \ No newline at end of file diff --git a/src/main/java/eu/m724/tweaks/TweaksConfig.java b/src/main/java/eu/m724/tweaks/TweaksConfig.java index 3a10a27..9c54a02 100644 --- a/src/main/java/eu/m724/tweaks/TweaksConfig.java +++ b/src/main/java/eu/m724/tweaks/TweaksConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Minecon724 + * Copyright (C) 2025 Minecon724 * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file * in the project root for the full license text. */ @@ -57,7 +57,10 @@ public record TweaksConfig( boolean redstoneEnabled, String redstoneListen, - Map knockbackModifiers + Map knockbackModifiers, + + boolean killswitchEnabled, + String killswitchListen ) { public static final int CONFIG_VERSION = 2; private static TweaksConfig config; @@ -129,6 +132,9 @@ public record TweaksConfig( // this is processed when initing Map knockbackModifiers = config.getConfigurationSection("knockback").getValues(false); + boolean killswitchEnabled = config.getBoolean("killswitch.enabled"); + String killswitchListen = config.getString("killswitch.listen"); + TweaksConfig.config = new TweaksConfig( debug, metrics, locale, worldborderExpand, worldborderHide, @@ -143,7 +149,8 @@ public record TweaksConfig( sleepEnabled, sleepInstant, authEnabled, authForce, authHostname, redstoneEnabled, redstoneListen, - knockbackModifiers + knockbackModifiers, + killswitchEnabled, killswitchListen ); return TweaksConfig.config; diff --git a/src/main/java/eu/m724/tweaks/TweaksPlugin.java b/src/main/java/eu/m724/tweaks/TweaksPlugin.java index 7c5e238..575b935 100644 --- a/src/main/java/eu/m724/tweaks/TweaksPlugin.java +++ b/src/main/java/eu/m724/tweaks/TweaksPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Minecon724 + * Copyright (C) 2025 Minecon724 * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file * in the project root for the full license text. */ @@ -14,6 +14,7 @@ import eu.m724.tweaks.door.DoorKnockListener; import eu.m724.tweaks.door.DoorOpenListener; import eu.m724.tweaks.full.FullListener; import eu.m724.tweaks.hardcore.HardcoreManager; +import eu.m724.tweaks.killswitch.KillswitchManager; import eu.m724.tweaks.knockback.KnockbackListener; import eu.m724.tweaks.motd.MotdManager; import eu.m724.tweaks.ping.F3NameListener; @@ -129,6 +130,11 @@ public class TweaksPlugin extends MStatsPlugin { DebugLogger.fine("Enabling Knockback"); new KnockbackListener(this); + if (config.killswitchEnabled()) { + DebugLogger.fine("Enabling Killswitch"); + new KillswitchManager(this).init(getCommand("servkill")); + } + /* end modules */ if (config.metrics()) { diff --git a/src/main/java/eu/m724/tweaks/killswitch/KillswitchManager.java b/src/main/java/eu/m724/tweaks/killswitch/KillswitchManager.java new file mode 100644 index 0000000..c16f75d --- /dev/null +++ b/src/main/java/eu/m724/tweaks/killswitch/KillswitchManager.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2025 Minecon724 + * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file + * in the project root for the full license text. + */ + +package eu.m724.tweaks.killswitch; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import eu.m724.tweaks.DebugLogger; +import eu.m724.tweaks.TweaksConfig; +import eu.m724.tweaks.TweaksPlugin; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; + +public class KillswitchManager implements CommandExecutor, HttpHandler { + private final Plugin plugin; + private final Ratelimit ratelimit = new Ratelimit(); + + private byte[] secret; + private String secretEncoded; + + public KillswitchManager(Plugin plugin) { + this.plugin = plugin; + } + + private void loadKey(File file) { + if (file.exists()) { + try { + this.secret = Files.readAllBytes(file.toPath()); + } catch (IOException e) { + throw new RuntimeException("Reading killswitch key", e); + } + DebugLogger.fine("Loaded key"); + } else { + byte[] buf = new byte[16]; + + try { + SecureRandom.getInstanceStrong().nextBytes(buf); + Files.write(file.toPath(), buf); + } catch (IOException | NoSuchAlgorithmException e) { + throw new RuntimeException("Generating killswitch key", e); + } + + this.secret = buf; + DebugLogger.info("Killswitch key generated and saved to " + file.getPath()); + } + + this.secretEncoded = Base64.getEncoder().encodeToString(secret); + } + + public void init(PluginCommand serverKillCommand) { + serverKillCommand.setExecutor(this); + + if (TweaksConfig.getConfig().killswitchListen() != null) { + loadKey(new File(plugin.getDataFolder(), "storage/killswitch key")); + + ratelimit.runTaskTimerAsynchronously(plugin, 0, 20 * 300); + + var listenAddress = TweaksConfig.getConfig().killswitchListen().split(":"); + InetSocketAddress bindAddress; + if (listenAddress.length == 1) { + bindAddress = new InetSocketAddress(Integer.parseInt(listenAddress[0])); + } else { + bindAddress = new InetSocketAddress(listenAddress[0], Integer.parseInt(listenAddress[1])); + } + + try { + HttpServer server = HttpServer.create(bindAddress, 0); + server.createContext("/", this); + server.setExecutor(null); + server.start(); + DebugLogger.fine("server started"); + } catch (IOException e) { + throw new RuntimeException("Starting HTTP server", e); + } + } + } + + private void kill() { + Runtime.getRuntime().halt(0); + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + kill(); + return true; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + exchange.sendResponseHeaders(404, -1); + + var path = exchange.getRequestURI().getPath(); + if (!path.startsWith("/key/")) return; + + var address = exchange.getRemoteAddress().getAddress(); + if (!ratelimit.submitRequest(address)) { + DebugLogger.fine(address + " is ratelimited"); + return; + } + + var key = path.substring(5); + if (key.equals(secretEncoded)) { + DebugLogger.fine("Got a request with valid key"); + kill(); + } else { + DebugLogger.fine("Got a request with invalid key"); + } + } +} diff --git a/src/main/java/eu/m724/tweaks/killswitch/Ratelimit.java b/src/main/java/eu/m724/tweaks/killswitch/Ratelimit.java new file mode 100644 index 0000000..40039b6 --- /dev/null +++ b/src/main/java/eu/m724/tweaks/killswitch/Ratelimit.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 Minecon724 + * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file + * in the project root for the full license text. + */ + +package eu.m724.tweaks.killswitch; + +import org.bukkit.scheduler.BukkitRunnable; + +import java.net.InetAddress; +import java.util.HashSet; +import java.util.Set; + +public class Ratelimit extends BukkitRunnable { + private Set requests = new HashSet<>(); + + boolean submitRequest(InetAddress address) { + return requests.add(address); + } + + @Override + public void run() { + requests = new HashSet<>(); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index bcf91fa..9c49759 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -104,6 +104,7 @@ auth: domain: "replace.me" # Adds gateways emitting redstone, controlled over internet +# https://git.m724.eu/Minecon724/tweaks724/src/branch/master/RETSTONE.md retstone: enabled: true # This takes host:port, listens on UDP @@ -117,6 +118,15 @@ knockback: tnt: 5 creeper: 0.7 +# Kills server after /servkill or HTTP request +# https://git.m724.eu/Minecon724/tweaks724/src/branch/master/KILLSWITCH.md +killswitch: + enabled: true + # This takes host:port, starts an HTTP server + # To disable HTTP server, set to null + listen: 127.0.0.1:57932 + + # Finally, thank you for downloading Tweaks724, I hope you enjoy! # Don't modify unless told to diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index e96250e..fac4b46 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -37,6 +37,9 @@ commands: retstone: description: Retstone commands permission: tweaks724.retstone + servkill: + description: Immediately stop the server + permission: tweaks724.servkill permissions: tweaks724: @@ -54,4 +57,5 @@ permissions: default: op retstone: default: op - + servkill: + default: false