From 54188d7c31578c77bfe2763be3df80a6a25896e0 Mon Sep 17 00:00:00 2001 From: Jonas_Jones Date: Sat, 2 Mar 2024 17:04:01 +0100 Subject: [PATCH] Implement Basic Auth for Webserver --- .../mcwebserver/config/ModConfigs.java | 3 + .../mcwebserver/web/HttpServer.java | 91 +++++++++++++------ .../mcwebserver/web/api/ApiRequests.java | 8 +- .../mcwebserver/web/api/ApiRequestsUtil.java | 11 ++- .../mcwebserver/web/api/ErrorHandler.java | 8 ++ .../mcwebserver/web/api/v2/ApiV2Handler.java | 21 ++++- 6 files changed, 106 insertions(+), 36 deletions(-) diff --git a/src/main/java/me/jonasjones/mcwebserver/config/ModConfigs.java b/src/main/java/me/jonasjones/mcwebserver/config/ModConfigs.java index bb994ba..09c270d 100644 --- a/src/main/java/me/jonasjones/mcwebserver/config/ModConfigs.java +++ b/src/main/java/me/jonasjones/mcwebserver/config/ModConfigs.java @@ -15,6 +15,7 @@ public class ModConfigs { public static String WEB_ROOT; public static String WEB_FILE_ROOT; public static String WEB_FILE_404; + public static Boolean WEB_REQUIRE_TOKEN; public static Boolean SERVER_API_ENABLED; public static Boolean ADV_API_ENABLED; public static Boolean API_INGAME_COMMAND_ENABLED; @@ -45,6 +46,7 @@ public class ModConfigs { config.addKeyValuePair(new Pair<>("web.root", "webserver/"), "the root directory of the webserver, starting from the main server directory"); config.addKeyValuePair(new Pair<>("web.file.root", "index.html"), "the name of the html file for the homepage"); config.addKeyValuePair(new Pair<>("web.file.404", "404.html"), "the name of the html file for 404 page"); + config.addKeyValuePair(new Pair<>("web.require_token", false), "wether or not you are required to provide a token to access files on the webserver"); 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"); @@ -59,6 +61,7 @@ public class ModConfigs { WEB_ROOT = CONFIG.getOrDefault("web.root", "webserver/"); WEB_FILE_ROOT = CONFIG.getOrDefault("web.file.root", "index.html"); WEB_FILE_404 = CONFIG.getOrDefault("web.file.404", "404.html"); + WEB_REQUIRE_TOKEN = CONFIG.getOrDefault("web.require_token", false); SERVER_API_ENABLED = CONFIG.getOrDefault("web.api", true); ADV_API_ENABLED = CONFIG.getOrDefault("web.api.adv", true); API_INGAME_COMMAND_ENABLED = CONFIG.getOrDefault("web.api.cmd", true); diff --git a/src/main/java/me/jonasjones/mcwebserver/web/HttpServer.java b/src/main/java/me/jonasjones/mcwebserver/web/HttpServer.java index 3d83548..f119f9b 100644 --- a/src/main/java/me/jonasjones/mcwebserver/web/HttpServer.java +++ b/src/main/java/me/jonasjones/mcwebserver/web/HttpServer.java @@ -8,6 +8,7 @@ import me.jonasjones.mcwebserver.web.api.v1.ApiV1Handler; import me.jonasjones.mcwebserver.web.api.ApiRequests; import me.jonasjones.mcwebserver.web.api.ApiRequestsUtil; import me.jonasjones.mcwebserver.web.api.v2.ApiV2Handler; +import me.jonasjones.mcwebserver.web.api.v2.tokenmgr.TokenManager; import java.io.BufferedOutputStream; import java.io.BufferedReader; @@ -22,6 +23,7 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.StringTokenizer; @@ -121,8 +123,8 @@ public class HttpServer implements Runnable { while ((header = in.readLine()) != null && !header.isEmpty()) { // Check if the header contains your API token - if (header.startsWith("Authorization: Bearer ")) { - apiToken = header.substring("Authorization: Bearer ".length()); + if (header.startsWith("Authorization: Basic ")) { + apiToken = header.substring("Authorization: Basic ".length()); } } @@ -238,35 +240,68 @@ public class HttpServer implements Runnable { fileRequested = fileRequested.substring(1); } - Path file = WEB_ROOT.resolve(fileRequested).toRealPath(LinkOption.NOFOLLOW_LINKS); - if (!file.startsWith(WEB_ROOT)) { - VerboseLogger.warn("Access to file outside root: " + file); - throw new NoSuchFileException(fileRequested); - } - int fileLength = (int) Files.size(file); - int fileExtensionStartIndex = fileRequested.lastIndexOf(".") + 1; - String contentType; - if (fileExtensionStartIndex > 0) { - contentType = mimetypeidentifier.compare(fileRequested.substring(fileExtensionStartIndex)); - } else { - contentType = "text/plain"; + try { + boolean isAuthorized = false; + if (!ModConfigs.WEB_REQUIRE_TOKEN) { + isAuthorized = true; + } else if (apiToken != null) { + isAuthorized = TokenManager.isTokenValid(apiToken); + } + + if (isAuthorized) { + + Path file = WEB_ROOT.resolve(fileRequested).toRealPath(LinkOption.NOFOLLOW_LINKS); + if (!file.startsWith(WEB_ROOT)) { + VerboseLogger.warn("Access to file outside root: " + file); + throw new NoSuchFileException(fileRequested); + } + int fileLength = (int) Files.size(file); + int fileExtensionStartIndex = fileRequested.lastIndexOf(".") + 1; + String contentType; + if (fileExtensionStartIndex > 0) { + contentType = mimetypeidentifier.compare(fileRequested.substring(fileExtensionStartIndex)); + } else { + contentType = "text/plain"; + } + + byte[] fileData = readFileData(file); + + // send HTTP Headers + dataOut.write(OK); + dataOut.write(HEADERS); + dataOut.write("Date: %s\r\n".formatted(Instant.now()).getBytes(StandardCharsets.UTF_8)); + dataOut.write("Content-Type: %s\r\n".formatted(contentType).getBytes(StandardCharsets.UTF_8)); + dataOut.write("Content-Length: %s\r\n".formatted(fileLength).getBytes(StandardCharsets.UTF_8)); + dataOut.write(CRLF); // blank line between headers and content, very important ! + if (method.equals("GET")) { // GET method so we return content + dataOut.write(fileData, 0, fileLength); + dataOut.flush(); + } + + VerboseLogger.info("File " + fileRequested + " of type " + contentType + " returned"); + } else { + dataOut.write("HTTP/1.1 200 OK\r\n".getBytes(StandardCharsets.UTF_8)); + dataOut.write("Date: %s\r\n".formatted(Instant.now()).getBytes(StandardCharsets.UTF_8)); + dataOut.write("Content-Type: application/json\r\n".getBytes(StandardCharsets.UTF_8)); + String jsonString = ErrorHandler.unauthorizedString(); + + + byte[] jsonBytes = jsonString.getBytes(StandardCharsets.UTF_8); + int contentLength = jsonBytes.length; + + dataOut.write(("Content-Length: " + contentLength + "\r\n").getBytes(StandardCharsets.UTF_8)); + dataOut.write("\r\n".getBytes(StandardCharsets.UTF_8)); // Blank line before content + + // Send JSON data + dataOut.write(jsonBytes, 0, contentLength); + dataOut.flush(); + } + + } catch (NoSuchAlgorithmException e) { + McWebserver.LOGGER.error("Error getting JSON data from ApiHandler: " + e.getMessage()); } - byte[] fileData = readFileData(file); - // send HTTP Headers - dataOut.write(OK); - dataOut.write(HEADERS); - dataOut.write("Date: %s\r\n".formatted(Instant.now()).getBytes(StandardCharsets.UTF_8)); - dataOut.write("Content-Type: %s\r\n".formatted(contentType).getBytes(StandardCharsets.UTF_8)); - dataOut.write("Content-Length: %s\r\n".formatted(fileLength).getBytes(StandardCharsets.UTF_8)); - dataOut.write(CRLF); // blank line between headers and content, very important ! - if (method.equals("GET")) { // GET method so we return content - dataOut.write(fileData, 0, fileLength); - dataOut.flush(); - } - - VerboseLogger.info("File " + fileRequested + " of type " + contentType + " returned"); } diff --git a/src/main/java/me/jonasjones/mcwebserver/web/api/ApiRequests.java b/src/main/java/me/jonasjones/mcwebserver/web/api/ApiRequests.java index d5e6bca..815657e 100644 --- a/src/main/java/me/jonasjones/mcwebserver/web/api/ApiRequests.java +++ b/src/main/java/me/jonasjones/mcwebserver/web/api/ApiRequests.java @@ -11,12 +11,16 @@ public class ApiRequests { return "[\"" + value + "\"]"; } + public static String playerLookupRequest(String playerName) { + return gson.toJson(ApiRequestsUtil.playerLookup(playerName)); + } + public static String playerNamesRequest() { return gson.toJsonTree(ApiRequestsUtil.convertPlayerList(ApiRequestsUtil.getSERVER_METADATA().players().get().sample())).getAsJsonArray().toString(); } - public static String playerInfoRequest(String playerName) { - return gson.toJson(ApiRequestsUtil.getPlayerInfo(playerName)); + public static String playerInfoAllRequest(String playerUuid) { + return gson.toJson(ApiRequestsUtil.getPlayerInfo(playerUuid)); } public static String serverMetadataRequest() { diff --git a/src/main/java/me/jonasjones/mcwebserver/web/api/ApiRequestsUtil.java b/src/main/java/me/jonasjones/mcwebserver/web/api/ApiRequestsUtil.java index 5cbafc5..376d3ad 100644 --- a/src/main/java/me/jonasjones/mcwebserver/web/api/ApiRequestsUtil.java +++ b/src/main/java/me/jonasjones/mcwebserver/web/api/ApiRequestsUtil.java @@ -124,9 +124,18 @@ public class ApiRequestsUtil { return ApiRequestsUtil.getSERVER_METADATA().favicon().get().iconBytes(); } - public static JsonObject getPlayerInfo(String playerName) { + public static JsonObject playerLookup(String playerName) { for (ServerPlayerEntity player : ApiRequestsUtil.getSERVER_PLAYER_ENTITY_LIST()) { if (player.getName().getString().equals(playerName)) { + return JsonParser.parseString("{\"uuid\":\"" + player.getUuidAsString() + "\"}").getAsJsonObject(); + } + } + return gson.toJsonTree(ErrorHandler.customError(404, "Player Not Found")).getAsJsonObject(); + } + + public static JsonObject getPlayerInfo(String uuid) { + for (ServerPlayerEntity player : ApiRequestsUtil.getSERVER_PLAYER_ENTITY_LIST()) { + if (player.getUuidAsString().equals(uuid)) { try { String sleepDirection = (player.getSleepingDirection() != null) ? player.getSleepingDirection().asString() : null; Vec sleepPosition = new Vec<>(); diff --git a/src/main/java/me/jonasjones/mcwebserver/web/api/ErrorHandler.java b/src/main/java/me/jonasjones/mcwebserver/web/api/ErrorHandler.java index 9fe87cb..0f3d1ae 100644 --- a/src/main/java/me/jonasjones/mcwebserver/web/api/ErrorHandler.java +++ b/src/main/java/me/jonasjones/mcwebserver/web/api/ErrorHandler.java @@ -13,6 +13,14 @@ public class ErrorHandler { return gson.toJsonTree(badRequest()).getAsJsonObject().toString(); } + public static Error unauthorized() { + return new Error(401, "Unauthorized"); + } + + public static String unauthorizedString() { + return gson.toJsonTree(unauthorized()).getAsJsonObject().toString(); + } + public static Error internalServerError() { return new Error(500, "Internal Server Error"); } diff --git a/src/main/java/me/jonasjones/mcwebserver/web/api/v2/ApiV2Handler.java b/src/main/java/me/jonasjones/mcwebserver/web/api/v2/ApiV2Handler.java index 7c29da9..aeab2d0 100644 --- a/src/main/java/me/jonasjones/mcwebserver/web/api/v2/ApiV2Handler.java +++ b/src/main/java/me/jonasjones/mcwebserver/web/api/v2/ApiV2Handler.java @@ -70,11 +70,22 @@ public class ApiV2Handler { if (isTokenValid) { request = request.replace("/api/v2/", ""); - if (request.startsWith("playerinfo?playername=")) { - String playerName = request.replace("playerinfo?playername=", ""); - return ApiRequests.playerInfoRequest(playerName); - } else if (request.startsWith("playerinfo?playeruuid=")) { - return ErrorHandler.badRequestString(); + if (request.startsWith("playerlookup?uuid=")) { + return ApiRequests.playerLookupRequest(request.replace("playerlookup?uuid=", "")); + } else if (request.startsWith("playerinfo/inventory?uuid=")) { + String playerName = request.replace("playerinfo/inventory?uuid=", ""); + //return ApiRequests.playerInfoRequest(playerName); + return ErrorHandler.internalServerErrorString(); + } else if (request.startsWith("playerinfo/inventory?playeruuid=")) { + //return ApiRequests.playerInfoRequestFromUuid(request.replace("playerinfo/inventory?playeruuid=", "")); + return ErrorHandler.internalServerErrorString(); + } else if (request.startsWith("playerinfo/enderchest?playername=")) { + String playerName = request.replace("playerinfo/enderchest?playername=", ""); + //return ApiRequests.playerInfoRequest(playerName); + return ErrorHandler.internalServerErrorString(); + } else if (request.startsWith("playerinfo/enderchest?playeruuid=")) { + //return ApiRequests.playerInfoRequestFromUuid(request.replace("playerinfo/enderchest?playeruuid=", "")); + return ErrorHandler.internalServerErrorString(); } else { return ErrorHandler.notFoundErrorString(); }