diff --git a/src/main/java/eu/m724/tweaks/TweaksConfig.java b/src/main/java/eu/m724/tweaks/TweaksConfig.java index 0a6bde7..a3702cb 100644 --- a/src/main/java/eu/m724/tweaks/TweaksConfig.java +++ b/src/main/java/eu/m724/tweaks/TweaksConfig.java @@ -42,7 +42,11 @@ public record TweaksConfig( float hardcoreChance, boolean sleepEnabled, - boolean sleepInstant + boolean sleepInstant, + + boolean authEnabled, + boolean authForce, + String authDomain ) { public static final int CONFIG_VERSION = 1; private static TweaksConfig config; @@ -99,6 +103,10 @@ public record TweaksConfig( boolean sleepEnabled = config.getBoolean("sleep.enabled"); boolean sleepInstant = config.getBoolean("sleep.instant"); + boolean authEnabled = config.getBoolean("auth.enabled"); + boolean authForce = config.getBoolean("auth.force"); + String authHostname = config.getString("auth.domain"); + TweaksConfig.config = new TweaksConfig( worldborderExpand, worldborderHide, brandEnabled, brandText, brandShowPing, brandShowMspt, @@ -109,7 +117,8 @@ public record TweaksConfig( pomodoroEnabled, pomodoroForce, updaterEnabled, hardcoreEnabled, hardcoreChance, - sleepEnabled, sleepInstant + sleepEnabled, sleepInstant, + authEnabled, authForce, authHostname ); return TweaksConfig.config; diff --git a/src/main/java/eu/m724/tweaks/TweaksPlugin.java b/src/main/java/eu/m724/tweaks/TweaksPlugin.java index 7e1e450..36bab87 100644 --- a/src/main/java/eu/m724/tweaks/TweaksPlugin.java +++ b/src/main/java/eu/m724/tweaks/TweaksPlugin.java @@ -6,6 +6,7 @@ package eu.m724.tweaks; +import eu.m724.tweaks.auth.AuthManager; import eu.m724.tweaks.chat.ChatCommands; import eu.m724.tweaks.chat.ChatManager; import eu.m724.tweaks.door.DoorManager; @@ -94,6 +95,10 @@ public class TweaksPlugin extends JavaPlugin { new SleepManager().init(this); } + if (config.authEnabled()) { + new AuthManager(this).init(getCommand("tauth")); + } + getLogger().info("Took %.3f milliseconds".formatted((System.nanoTime() - start) / 1000000.0)); } diff --git a/src/main/java/eu/m724/tweaks/auth/AuthCommands.java b/src/main/java/eu/m724/tweaks/auth/AuthCommands.java new file mode 100644 index 0000000..5463fb4 --- /dev/null +++ b/src/main/java/eu/m724/tweaks/auth/AuthCommands.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 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.auth; + +import eu.m724.tweaks.TweaksConfig; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.FileNotFoundException; +import java.util.UUID; + +public class AuthCommands implements CommandExecutor { + private final AuthStorage authStorage; + + AuthCommands(AuthStorage authStorage) { + this.authStorage = authStorage; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (args.length == 0) { + sender.sendMessage(" | new | delete"); + return true; + } + + String action = args[0]; + + if (action.equals("new")) { + String key = authStorage.generateKey(); + authStorage.create(key); + String hostname = key + "." + TweaksConfig.getConfig().authDomain(); + + if (sender instanceof Player) { + BaseComponent component = new ComponentBuilder("Generated a new key. Click to copy.") + .underlined(true) + .color(ChatColor.GRAY) + .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, hostname)) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to copy"))) + .build(); + sender.spigot().sendMessage(component); + } else { + sender.sendMessage("Generated a new key: " + hostname); + } + } else if (action.equals("delete")) { + if (args.length == 1) { + sender.sendMessage("You need to pass the key you want to delete as an argument"); + return true; + } + + UUID user = authStorage.delete(args[1].split("\\.")[0]); + + if (user != null) { + if (user.getLeastSignificantBits() == 0 && user.getMostSignificantBits() == 0) { + sender.sendMessage("Key deleted. It wasn't assigned to any player."); + } else { + sender.sendMessage("Key deleted. It was assigned to " + Bukkit.getOfflinePlayer(user).getName()); + } + } else { + sender.sendMessage("There's no such key."); + } + } else { + try { + UUID user = authStorage.getUserOfKey(action.split("\\.")[0]); + + if (user == null) { + sender.sendMessage("Key has no owner, meaning anybody who uses it gets to keep it"); + } else { + OfflinePlayer player = Bukkit.getOfflinePlayer(user); + sender.sendMessage("Key is assigned to %s %s".formatted(player.getName(), user)); + } + } catch (FileNotFoundException | AuthStorage.InvalidKeyException e) { + sender.sendMessage("No such key. Enter a valid key. Or you meant 'new' or 'delete'?"); + } + } + + return true; + } +} diff --git a/src/main/java/eu/m724/tweaks/auth/AuthListener.java b/src/main/java/eu/m724/tweaks/auth/AuthListener.java new file mode 100644 index 0000000..1ed2a4f --- /dev/null +++ b/src/main/java/eu/m724/tweaks/auth/AuthListener.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 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.auth; + +import eu.m724.tweaks.Language; +import eu.m724.tweaks.TweaksConfig; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerLoginEvent; + +import java.io.FileNotFoundException; + +public class AuthListener implements Listener { + private final AuthStorage authStorage; + private final boolean force = TweaksConfig.getConfig().authForce(); + + public AuthListener(AuthStorage authStorage) { + this.authStorage = authStorage; + } + + @EventHandler + public void onPlayerLogin(PlayerLoginEvent event) { + Player player = event.getPlayer(); + + String key = event.getHostname().split("\\.")[0]; + + boolean allowed = false; + String expected = authStorage.getKeyOfUser(player.getUniqueId()); + + if (key.equals(expected)) { + allowed = true; // key matches player + } else if (expected == null) { // player doesn't have key + try { + authStorage.assignOwner(key, player.getUniqueId()); + allowed = true; // key just assigned + } catch (FileNotFoundException | AuthStorage.AlreadyClaimedException | AuthStorage.InvalidKeyException e) { + allowed = !force; // If forced all players must have a key + } + } + + if (!allowed) { + if (expected == null) { // if player is new + event.disallow(PlayerLoginEvent.Result.KICK_WHITELIST, Language.getString("authKickUnregistered")); + } else { + event.disallow(PlayerLoginEvent.Result.KICK_OTHER, Language.getString("authKickWrongKey")); + } + } + } +} diff --git a/src/main/java/eu/m724/tweaks/auth/AuthManager.java b/src/main/java/eu/m724/tweaks/auth/AuthManager.java new file mode 100644 index 0000000..402b376 --- /dev/null +++ b/src/main/java/eu/m724/tweaks/auth/AuthManager.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 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.auth; + +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.Plugin; + +public class AuthManager { + private final AuthStorage authStorage; + private final Plugin plugin; + + public AuthManager(Plugin plugin) { + this.plugin = plugin; + this.authStorage = new AuthStorage(plugin); + } + + public void init(PluginCommand command) { + plugin.getServer().getPluginManager().registerEvents(new AuthListener(authStorage), plugin); + command.setExecutor(new AuthCommands(authStorage)); + } +} diff --git a/src/main/java/eu/m724/tweaks/auth/AuthStorage.java b/src/main/java/eu/m724/tweaks/auth/AuthStorage.java new file mode 100644 index 0000000..3b08b6a --- /dev/null +++ b/src/main/java/eu/m724/tweaks/auth/AuthStorage.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2024 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.auth; + +import org.bukkit.plugin.Plugin; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Random; +import java.util.UUID; + +public class AuthStorage { + private final File playersDirectory; + private final File keysDirectory; + + AuthStorage(Plugin plugin) { + File directory = new File(plugin.getDataFolder(), "auth storage"); + this.playersDirectory = new File(directory, "players"); + this.keysDirectory = new File(directory, "keys"); + + directory.mkdir(); + keysDirectory.mkdir(); + playersDirectory.mkdir(); + } + + private boolean isInvalid(String key) { + return !key.matches("^[a-zA-Z0-9]{4,100}$"); + } + + /** + * Gets the owner of a key + * @param key the key + * @return the owner's UUID or null if no owner + * @throws FileNotFoundException if no such key + */ + UUID getUserOfKey(String key) throws FileNotFoundException { + if (isInvalid(key)) throw new InvalidKeyException(); + + File file = new File(keysDirectory, key); + if (!file.exists()) throw new FileNotFoundException(); + + byte[] buf = new byte[16]; + + try (FileInputStream is = new FileInputStream(file)) { + int read = is.read(buf); + if (read < 16) return null; + } catch (IOException e) { + throw new RuntimeException(e); // TODO + } + + ByteBuffer byteBuffer = ByteBuffer.wrap(buf); + long msb = byteBuffer.getLong(); + long lsb = byteBuffer.getLong(); + return new UUID(msb, lsb); + } + + /** + * Gets the key of a user + * @param uuid the user's UUID + * @return the key of the user or null if no owned key + */ + String getKeyOfUser(UUID uuid) { + File file = new File(playersDirectory, uuid.toString()); + if (!file.exists()) return null; + + try (FileInputStream is = new FileInputStream(file)) { + byte[] bytes = is.readNBytes(50); + return new String(bytes, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); // TODO + } + + } + + /** + * Create a key that has no owner + * @param key the key to create + */ + void create(String key) { + if (isInvalid(key)) throw new InvalidKeyException(); + + File file = new File(keysDirectory, key); + try { + file.createNewFile(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Deletes a key + * @param key the key + * @return the user that was using this key, UUID(0, 0) if none, null if key didn't exist + */ + UUID delete(String key) { + if (isInvalid(key)) return null; + + UUID user = null; + + try { + user = getUserOfKey(key); + if (user != null) { + new File(playersDirectory, user.toString()).delete(); + } + } catch (FileNotFoundException e) { } + + if (!new File(keysDirectory, key).delete()) + return null; + + return user == null ? new UUID(0, 0) : user; + } + + /** + * Assigns an owner to a key + * @param key the key + * @param uuid the owner's UUID + * @throws FileNotFoundException if no such key + * @throws AlreadyClaimedException if key is claimed or user owns another key + */ + void assignOwner(String key, UUID uuid) throws FileNotFoundException, AlreadyClaimedException { + if (isInvalid(key)) throw new InvalidKeyException(); + + if (getUserOfKey(key) != null) throw new AlreadyClaimedException(); + if (getKeyOfUser(uuid) != null) throw new AlreadyClaimedException(); + + File file = new File(keysDirectory, key); + if (!file.exists()) throw new FileNotFoundException(); + + ByteBuffer byteBuffer = ByteBuffer.allocate(16); + byteBuffer.putLong(uuid.getMostSignificantBits()); + byteBuffer.putLong(uuid.getLeastSignificantBits()); + + try (FileOutputStream os = new FileOutputStream(file)) { + os.write(byteBuffer.array()); + } catch (IOException e) { + throw new RuntimeException(e); // TODO + } + + File file2 = new File(playersDirectory, uuid.toString()); + + try (FileOutputStream os = new FileOutputStream(file2)) { + os.write(key.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); // TODO + } + } + + // TODO improve + String generateKey() { + char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray(); + Random random = new Random(); + int length = random.nextInt(8, 10); + + StringBuilder key = new StringBuilder(); + + for (int i=0; i