Auth
Some checks failed
/ deploy (push) Has been cancelled

This commit is contained in:
Minecon724 2024-12-06 17:13:33 +01:00
parent 799833b685
commit 29df176ff7
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
9 changed files with 379 additions and 5 deletions

View file

@ -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;

View file

@ -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));
}

View file

@ -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("<key> | 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;
}
}

View file

@ -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"));
}
}
}
}

View file

@ -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));
}
}

View file

@ -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<length; i++) {
key.append(chars[random.nextInt(chars.length)]);
}
return key.toString();
}
static class InvalidKeyException extends RuntimeException {}
static class AlreadyClaimedException extends Exception {}
}

View file

@ -88,6 +88,16 @@ sleep:
# If instant: how much % of players to skip the night
# If not: how much % make skipping full speed
# "Hostname" authentication
# This makes a player need to join a unique hostname like "23sedf345.myserver.com" where "23sedf345" is the key
auth:
enabled: true
# All players must use a key. So new players will need a new key
force: false
# The domain of the server, if it's myserver.com codes will be like 12d3123.myserver.com
# This doesn't do anything other than showing in /tauth new
domain: "replace.me"
# Finally, thank you for downloading Tweaks724, I hope you enjoy!
# Don't modify unless told to

View file

@ -27,14 +27,17 @@ commands:
description: See available plugin updates
permission: tweaks724.updates
aliases: [pluginupdates]
tauth:
description: Authentication management
permission: tweaks724.tauth
permissions:
tweaks724.chatmanage:
default: true
tweaks724.dkick:
default: op
tweaks724.pomodoro:
default: true
tweaks724.updates:
default: op
tweaks724.tauth:
default: op

View file

@ -21,4 +21,9 @@ chatNoSuchRoom = No room named %s
chatAlreadyHere = You're already in this room
# Room name is added at end
chatJoined = Joined chat room:
chatPlayers = %d other players are here
chatPlayers = %d other players are here
# Used when a player joins using the wrong key or no key
authKickWrongKey = You're connecting to the wrong server address. You must connect to the one you're registered to.
# If force is enabled and player is not registered. Changing this reveals you're using this plugin
authKickUnregistered = You are not whitelisted on this server!