added token system implementation

This commit is contained in:
Jonas_Jones 2024-01-11 02:33:09 +01:00
parent cee5af6617
commit 6cdf285613
7 changed files with 435 additions and 3 deletions

View file

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

View file

@ -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<Suggestions> suggestExpirationTimes(CommandContext<ServerCommandSource> 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<Suggestions> suggestTokenNames(CommandContext<ServerCommandSource> serverCommandSourceCommandContext, SuggestionsBuilder builder) {
String[] tokenNames = getTokenNames();
for (String tokenName : tokenNames) {
builder.suggest(tokenName);
}
return builder.buildFuture();
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Token> readTokensFromFile() {
ArrayList<Token> 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<Token> 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<Token> 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();
}
}
}