From 6cdf2856133b4dc01b5408a84a74de404fda23ef Mon Sep 17 00:00:00 2001 From: Jonas_Jones <91549607+JonasunderscoreJones@users.noreply.github.com> Date: Thu, 11 Jan 2024 02:33:09 +0100 Subject: [PATCH] added token system implementation --- .../jonasjones/mcwebserver/McWebserver.java | 18 +- .../mcwebserver/commands/McWebCommand.java | 141 ++++++++++++++ .../mcwebserver/config/ModConfigs.java | 5 +- .../mcwebserver/web/api/v1/ApiHandler.java | 3 + .../web/api/v2/tokenmgr/Token.java | 19 ++ .../web/api/v2/tokenmgr/TokenManager.java | 180 ++++++++++++++++++ .../web/api/v2/tokenmgr/TokenSaveManager.java | 72 +++++++ 7 files changed, 435 insertions(+), 3 deletions(-) create mode 100644 src/main/java/me/jonasjones/mcwebserver/commands/McWebCommand.java create mode 100644 src/main/java/me/jonasjones/mcwebserver/web/api/v2/tokenmgr/Token.java create mode 100644 src/main/java/me/jonasjones/mcwebserver/web/api/v2/tokenmgr/TokenManager.java create mode 100644 src/main/java/me/jonasjones/mcwebserver/web/api/v2/tokenmgr/TokenSaveManager.java diff --git a/src/main/java/me/jonasjones/mcwebserver/McWebserver.java b/src/main/java/me/jonasjones/mcwebserver/McWebserver.java index 5dac249..1058512 100644 --- a/src/main/java/me/jonasjones/mcwebserver/McWebserver.java +++ b/src/main/java/me/jonasjones/mcwebserver/McWebserver.java @@ -1,16 +1,21 @@ package me.jonasjones.mcwebserver; import me.jonasjones.mcwebserver.config.ModConfigs; -import me.jonasjones.mcwebserver.web.HttpServer; import me.jonasjones.mcwebserver.web.api.v1.ApiHandler; import me.jonasjones.mcwebserver.web.ServerHandler; +import me.jonasjones.mcwebserver.web.api.v2.tokenmgr.Token; +import me.jonasjones.mcwebserver.web.api.v2.tokenmgr.TokenManager; import net.fabricmc.api.ModInitializer; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.TimeUnit; +import java.util.ArrayList; +import static me.jonasjones.mcwebserver.commands.McWebCommand.registerCommands; import static me.jonasjones.mcwebserver.config.ModConfigs.*; +import static me.jonasjones.mcwebserver.web.api.v2.tokenmgr.TokenSaveManager.readOrCreateTokenFile; public class McWebserver implements ModInitializer { // This logger is used to write text to the console and the log file. @@ -20,6 +25,7 @@ public class McWebserver implements ModInitializer { public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); public static final Logger VERBOSELOGGER = LoggerFactory.getLogger(MOD_ID + " - VERBOSE LOGGER"); public static Boolean ISFIRSTSTART = false; + public static MinecraftServer MC_SERVER; @Override public void onInitialize() { @@ -35,6 +41,14 @@ public class McWebserver implements ModInitializer { } if (SERVER_API_ENABLED) { + if (API_INGAME_COMMAND_ENABLED) { + ArrayList< Token > tokens = readOrCreateTokenFile(); + LOGGER.info("Loaded " + tokens.size() + " tokens from file."); + // register commands + registerCommands(); + } + + //start collecting api info ApiHandler.startHandler(); LOGGER.info("Server API enabled!"); diff --git a/src/main/java/me/jonasjones/mcwebserver/commands/McWebCommand.java b/src/main/java/me/jonasjones/mcwebserver/commands/McWebCommand.java new file mode 100644 index 0000000..b4a292c --- /dev/null +++ b/src/main/java/me/jonasjones/mcwebserver/commands/McWebCommand.java @@ -0,0 +1,141 @@ +package me.jonasjones.mcwebserver.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import me.jonasjones.mcwebserver.McWebserver; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.command.argument.MessageArgumentType; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import static me.jonasjones.mcwebserver.McWebserver.MC_SERVER; +import static me.jonasjones.mcwebserver.web.api.v2.tokenmgr.TokenManager.*; +import static net.minecraft.server.command.CommandManager.*; + +public class McWebCommand { + public static void registerCommands() { + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(literal("mcweb") + .then(literal("token") + .then(literal("new") + .then(CommandManager.argument("Name", StringArgumentType.word()) + .then(CommandManager.argument("Expiration Time (example: 1y3d4h -> 1 Year, 3 Days, 4 Hours)", StringArgumentType.word()) + .suggests(ExpirationTimeArgumentType::suggestExpirationTimes) + .executes(context -> { + String name = StringArgumentType.getString(context, "Name"); + String expires = StringArgumentType.getString(context, "Expiration Time (example: 1y3d4h -> 1 Year, 3 Days, 4 Hours)"); + String result = registerToken(name, expires); + if (result.equals("exists")) { + context.getSource().sendFeedback(() -> Text.of("A token with that name already exists"), false); + return 0; + } else if (result.equals("failed")) { + context.getSource().sendFeedback(() -> Text.of("Failed to create token (Unknown Error)"), false); + return 0; + } else { + context.getSource().sendFeedback(() -> Text.of("Token Created!\nExpires " + convertToHumanReadable(convertExpirationDate(expires))), true); + if (MC_SERVER != null) { + // get the player name + String playerName = Objects.requireNonNull(context.getSource().getPlayer()).getName().getString(); + MC_SERVER.getCommandManager().executeWithPrefix(MC_SERVER.getCommandSource(), "tellraw " + playerName + " [\"\",\"Token (will only show once): \",\"\\n\",\"[\",{\"text\":\"" + result + "\",\"color\":\"green\",\"clickEvent\":{\"action\":\"copy_to_clipboard\",\"value\":\"\"},\"hoverEvent\":{\"action\":\"show_text\",\"contents\":[\"Click to Copy to Clipboard\"]}},\"]\"]"); + return 1; + } + context.getSource().sendFeedback(() -> Text.of("Failed to create token (Unknown Error)"), false); + return 0; + } + })))) + .then(literal("list") + .executes(context -> { + context.getSource().sendFeedback(() -> Text.of(listTokens()), false); + return 1; + })) + .then(literal("delete") + .then(argument("Token Name", StringArgumentType.word()) + .suggests(DeleteTokenNameArgumentType::suggestTokenNames) + .executes(context -> { + String name = StringArgumentType.getString(context, "Token Name"); + if (deleteToken(name)) { + context.getSource().sendFeedback(() -> Text.of("Token '" + name + "' deleted!"), true); + return 1; + } else { + context.getSource().sendFeedback(() -> Text.of("Token not found!"), false); + return 0; + } + })))))); + } + + public static class ExpirationTimeArgumentType { + + public static ExpirationTimeArgumentType word() { + return new ExpirationTimeArgumentType(); + } + private boolean isValid(String input) { + // The regex pattern for your requirements. + String pattern = "^(\\d+h)?(\\d+d)?(\\d+y)?$"; + + // Check if the input matches the pattern. + //return input.matches(pattern); + return input.equals("1h"); + } + + static CompletableFuture suggestExpirationTimes(CommandContext serverCommandSourceCommandContext, SuggestionsBuilder builder) { + try { + // get the current input + String input = StringArgumentType.getString(serverCommandSourceCommandContext, "Expiration Time (example: 1y3d4h -> 1 Year, 3 Days, 4 Hours)"); + + + // check if the input matches the pattern + if (!input.equals("0")) { + if (input.matches("\\d+")) { + builder.suggest(input + "y"); + builder.suggest(input + "d"); + builder.suggest(input + "h"); + suggestIntRange(builder, input, 0); + } else if (input.matches("\\d+y\\d+")) { + builder.suggest(input + "y"); + builder.suggest(input + "d"); + suggestIntRange(builder, input, 0); + } else if (input.matches("\\d+d\\d+")) { + builder.suggest(input + "d"); + builder.suggest(input + "h"); + suggestIntRange(builder, input, 0); + } else if (input.matches("\\d+y\\d+d\\d+")) { + builder.suggest(input + "h"); + suggestIntRange(builder, input, 0); + } else if (input.matches("\\d+y")) { + suggestIntRange(builder, input, 1); + } else if (input.matches("\\d+d")) { + suggestIntRange(builder, input, 1); + } else if (input.matches("\\d+y\\d+d")) { + suggestIntRange(builder, input, 1); + } + } + } catch (IllegalArgumentException e) { + suggestIntRange(builder, "", 0); + } + return builder.buildFuture(); + } + + private static void suggestIntRange(SuggestionsBuilder builder, String input, int min) { + for (int i = min; i <= 9; i++) { + builder.suggest(input + String.valueOf(i)); + } + } + } + + public static class DeleteTokenNameArgumentType { + static CompletableFuture suggestTokenNames(CommandContext serverCommandSourceCommandContext, SuggestionsBuilder builder) { + String[] tokenNames = getTokenNames(); + + for (String tokenName : tokenNames) { + builder.suggest(tokenName); + } + return builder.buildFuture(); + } + } +} diff --git a/src/main/java/me/jonasjones/mcwebserver/config/ModConfigs.java b/src/main/java/me/jonasjones/mcwebserver/config/ModConfigs.java index 8fbfbb6..3197c9f 100644 --- a/src/main/java/me/jonasjones/mcwebserver/config/ModConfigs.java +++ b/src/main/java/me/jonasjones/mcwebserver/config/ModConfigs.java @@ -17,6 +17,7 @@ public class ModConfigs { public static String WEB_FILE_404; public static Boolean SERVER_API_ENABLED; public static Boolean ADV_API_ENABLED; + public static Boolean API_INGAME_COMMAND_ENABLED; public static String WEB_FILE_NOSUPPORT; public static Boolean VERBOSE = false; //needs to be set to false since the verbose logger is called before config file is fully loaded @@ -44,6 +45,7 @@ public class ModConfigs { config.addKeyValuePair(new Pair<>("web.file.404", "404.html"), "the name of the html file for 404 page"); config.addKeyValuePair(new Pair<>("web.api", true), "whether or not the webserver api should be enabled or not"); config.addKeyValuePair(new Pair<>("web.api.adv", true), "whether or not the api should expose information such as player coordinates and inventory"); + config.addKeyValuePair(new Pair<>("web.api.cmd", true), "whether or not the ingame command to manage tokens should be enabled or not"); config.addKeyValuePair(new Pair<>("web.file.notSupported", "not_supported.html"), "the name of the html file for 'not supported' page"); config.addKeyValuePair(new Pair<>("logger.verbose", false), "whether or not to log verbose output"); } @@ -55,7 +57,8 @@ public class ModConfigs { WEB_FILE_ROOT = CONFIG.getOrDefault("web.file.root", "index.html"); WEB_FILE_404 = CONFIG.getOrDefault("web.file.404", "404.html"); SERVER_API_ENABLED = CONFIG.getOrDefault("web.api", true); - ADV_API_ENABLED = CONFIG.getOrDefault("web.api.adv", false); + ADV_API_ENABLED = CONFIG.getOrDefault("web.api.adv", true); + API_INGAME_COMMAND_ENABLED = CONFIG.getOrDefault("web.api.cmd", true); WEB_FILE_NOSUPPORT = CONFIG.getOrDefault("web.file.notSupported", "not_supported.html"); VERBOSE = CONFIG.getOrDefault("logger.verbose", true); } diff --git a/src/main/java/me/jonasjones/mcwebserver/web/api/v1/ApiHandler.java b/src/main/java/me/jonasjones/mcwebserver/web/api/v1/ApiHandler.java index 8403680..5a5572f 100644 --- a/src/main/java/me/jonasjones/mcwebserver/web/api/v1/ApiHandler.java +++ b/src/main/java/me/jonasjones/mcwebserver/web/api/v1/ApiHandler.java @@ -2,6 +2,8 @@ package me.jonasjones.mcwebserver.web.api.v1; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import static me.jonasjones.mcwebserver.McWebserver.MC_SERVER; + public class ApiHandler { public static Boolean isApiRequest(String request) { @@ -64,6 +66,7 @@ public class ApiHandler { public static void startHandler() { ServerTickEvents.END_SERVER_TICK.register(server -> { if (server.isRunning()) { + MC_SERVER = server; ApiRequestsUtil.setMOTD(server.getServerMotd()); ApiRequestsUtil.setSERVER_IP(server.getServerIp()); ApiRequestsUtil.setSERVER_PORT(server.getServerPort()); diff --git a/src/main/java/me/jonasjones/mcwebserver/web/api/v2/tokenmgr/Token.java b/src/main/java/me/jonasjones/mcwebserver/web/api/v2/tokenmgr/Token.java new file mode 100644 index 0000000..239d3d1 --- /dev/null +++ b/src/main/java/me/jonasjones/mcwebserver/web/api/v2/tokenmgr/Token.java @@ -0,0 +1,19 @@ +package me.jonasjones.mcwebserver.web.api.v2.tokenmgr; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class Token { + private String name; + private String tokenHash; + private String tokenStart; + private long expires; + + public Token(String name, String tokenHash, String tokenStart, long expires) { + this.name = name; + this.tokenHash = tokenHash; + this.tokenStart = tokenStart; + this.expires = expires; + } +} diff --git a/src/main/java/me/jonasjones/mcwebserver/web/api/v2/tokenmgr/TokenManager.java b/src/main/java/me/jonasjones/mcwebserver/web/api/v2/tokenmgr/TokenManager.java new file mode 100644 index 0000000..3f065bf --- /dev/null +++ b/src/main/java/me/jonasjones/mcwebserver/web/api/v2/tokenmgr/TokenManager.java @@ -0,0 +1,180 @@ +package me.jonasjones.mcwebserver.web.api.v2.tokenmgr; + +import lombok.Getter; +import me.jonasjones.mcwebserver.McWebserver; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static me.jonasjones.mcwebserver.web.api.v2.tokenmgr.TokenSaveManager.*; + +public class TokenManager { + @Getter + private static ArrayList tokens = new ArrayList<>(); + + private static String hashString(String input) throws NoSuchAlgorithmException { + try { + // Create a MessageDigest instance for SHA-256 + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + + // Update the digest with the input string + byte[] hashedBytes = digest.digest(input.getBytes()); + + // Convert the byte array to a hexadecimal string + StringBuilder hexString = new StringBuilder(); + for (byte b : hashedBytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + return hexString.toString(); + + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + private static String generateToken() { + try { + // Generate random bytes using SecureRandom + SecureRandom secureRandom = new SecureRandom(); + byte[] randomBytes = new byte[16]; // 16 bytes for a 128-bit hash + secureRandom.nextBytes(randomBytes); + + return hashString(new String(randomBytes)); + + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static long convertExpirationDate(String expiresIn) { + if (expiresIn == null || expiresIn.equals("0")) { + return 0; + } + try { + long timestamp = Long.parseLong(expiresIn); + if (timestamp > Instant.now().getEpochSecond()) { + // The input is already a future timestamp + return timestamp; + } + } catch (NumberFormatException ignored) { + // Input is not a valid timestamp, continue to parse the duration format + } + // Pattern to match the duration format (XyXdXh) + Pattern pattern = Pattern.compile("(\\d+)?[yY]?(\\d+)?[dD]?(\\d+)?[hH]?"); + Matcher matcher = pattern.matcher(expiresIn); + + int years = 0, days = 0, hours = 0; + + // Check if the input matches the pattern + if (matcher.matches()) { + String yearsStr = matcher.group(1); + String daysStr = matcher.group(2); + String hoursStr = matcher.group(3); + + // Parse and add the corresponding values + years = yearsStr != null ? Integer.parseInt(yearsStr) : 0; + days = daysStr != null ? Integer.parseInt(daysStr) : 0; + hours = hoursStr != null ? Integer.parseInt(hoursStr) : 0; + } + + // Calculate the future timestamp based on the current timestamp and the parsed values + long currentTimestamp = Instant.now().getEpochSecond(); + long futureTimestamp = currentTimestamp + (years * 365 * 24 * 60 * 60) + (days * 24 * 60 * 60) + (hours * 60 * 60); + + return futureTimestamp; + } + + public static String convertToHumanReadable(long unixTimestamp) { + if (unixTimestamp == 0) { + return "Never"; + } + // Convert Unix timestamp to LocalDateTime + Instant instant = Instant.ofEpochSecond(unixTimestamp); + LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + + // Define a format for the human-readable date-time + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + // Format the LocalDateTime to a string + return localDateTime.format(formatter) + " (UTC)"; + } + + public static String registerToken(String name, String expires) { + tokens = readTokensFromFile(); + // check if token already exists + for (Token tokenObj : tokens) { + if (tokenObj.getName().equals(name)) { + + return "exists"; + } + } + try { + String token = generateToken(); + String tokenStart = token.substring(0, 5); + Token tokenObj = new Token(name, hashString(token), tokenStart, convertExpirationDate(expires)); + tokens.add(tokenObj); + writeTokensToFile(tokens); + return token; + } catch (Exception e) { + McWebserver.LOGGER.error("Error generating token: " + e.getMessage()); + } + return "failed"; + } + + public static String listTokens() { + tokens = readTokensFromFile(); + StringBuilder sb = new StringBuilder(); + if (tokens.size() == 0) { + return "No active tokens."; + } + sb.append("Active Tokens:\n"); + sb.append("Name | Expiration Date | Beginning of Token value\n"); + for (Token token : tokens) { + sb.append(token.getName()).append(" | ").append(convertToHumanReadable(token.getExpires())).append(" | ").append(token.getTokenStart()).append("...").append("\n"); + } + return sb.toString(); + } + + public static Boolean deleteToken(String name) { + tokens = readTokensFromFile(); + for (Token token : tokens) { + if (token.getName().equals(name)) { + tokens.remove(token); + writeTokensToFile(tokens); + return true; + } + } + return false; + } + + public static String getToken(String name) { + tokens = readTokensFromFile(); + for (Token token : tokens) { + if (token.getName().equals(name)) { + return token.getTokenHash(); + } + } + return null; + } + + public static String[] getTokenNames() { + tokens = readTokensFromFile(); + String[] tokenNames = new String[tokens.size()]; + for (int i = 0; i < tokens.size(); i++) { + tokenNames[i] = tokens.get(i).getName(); + } + return tokenNames; + } +} diff --git a/src/main/java/me/jonasjones/mcwebserver/web/api/v2/tokenmgr/TokenSaveManager.java b/src/main/java/me/jonasjones/mcwebserver/web/api/v2/tokenmgr/TokenSaveManager.java new file mode 100644 index 0000000..5b811a7 --- /dev/null +++ b/src/main/java/me/jonasjones/mcwebserver/web/api/v2/tokenmgr/TokenSaveManager.java @@ -0,0 +1,72 @@ +package me.jonasjones.mcwebserver.web.api.v2.tokenmgr; + +import me.jonasjones.mcwebserver.McWebserver; +import net.fabricmc.loader.api.FabricLoader; + +import java.io.*; +import java.time.Instant; +import java.util.ArrayList; + +public class TokenSaveManager { + private static final String TOKEN_FILE_PATH = String.valueOf(FabricLoader.getInstance().getConfigDir()) + "/mcwebserver_tokens.txt"; + + public static Boolean isExpired(Token token) { + if (token.getExpires() == 0) { + return false; + } + return token.getExpires() < Instant.now().getEpochSecond(); + } + public static ArrayList readTokensFromFile() { + ArrayList tokenList = new ArrayList<>(); + + try (BufferedReader reader = new BufferedReader(new FileReader(TOKEN_FILE_PATH))) { + String line; + while ((line = reader.readLine()) != null) { + String[] parts = line.split("\\|"); + if (parts.length == 4 && !parts[0].equals("null")) { + String name = parts[0]; + String token = parts[1]; + String tokenStart = parts[2]; + long expires = Long.parseLong(parts[3]); + + Token tokenObj = new Token(name, token, tokenStart, expires); + + if (!isExpired(tokenObj)) { + tokenList.add(tokenObj); + } + } + } + } catch (IOException | NumberFormatException e) { + McWebserver.LOGGER.error("Error reading tokens from file: " + e.getMessage()); + } + + return tokenList; + } + + public static void writeTokensToFile(ArrayList tokenList) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(TOKEN_FILE_PATH))) { + for (Token token : tokenList) { + String line = token.getName() + "|" + token.getTokenHash() + "|" + token.getTokenStart() + "|" + token.getExpires(); + writer.write(line); + writer.newLine(); + } + } catch (IOException e) { + McWebserver.LOGGER.error("Error writing tokens to file: " + e.getMessage()); + } + } + + public static ArrayList readOrCreateTokenFile() { + File file = new File(TOKEN_FILE_PATH); + if (!file.exists()) { + try { + file.createNewFile(); + McWebserver.LOGGER.info("Created api token file."); + } catch (IOException e) { + McWebserver.LOGGER.error("Error creating api token file: " + e.getMessage()); + } + return new ArrayList<>(); + } else { + return readTokensFromFile(); + } + } +}