Implement Basic Auth for Webserver

This commit is contained in:
Jonas_Jones 2024-03-02 17:04:01 +01:00
parent aa51d85388
commit 54188d7c31
6 changed files with 106 additions and 36 deletions

View file

@ -15,6 +15,7 @@ public class ModConfigs {
public static String WEB_ROOT; public static String WEB_ROOT;
public static String WEB_FILE_ROOT; public static String WEB_FILE_ROOT;
public static String WEB_FILE_404; public static String WEB_FILE_404;
public static Boolean WEB_REQUIRE_TOKEN;
public static Boolean SERVER_API_ENABLED; public static Boolean SERVER_API_ENABLED;
public static Boolean ADV_API_ENABLED; public static Boolean ADV_API_ENABLED;
public static Boolean API_INGAME_COMMAND_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.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.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.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", 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.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.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_ROOT = CONFIG.getOrDefault("web.root", "webserver/");
WEB_FILE_ROOT = CONFIG.getOrDefault("web.file.root", "index.html"); WEB_FILE_ROOT = CONFIG.getOrDefault("web.file.root", "index.html");
WEB_FILE_404 = CONFIG.getOrDefault("web.file.404", "404.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); SERVER_API_ENABLED = CONFIG.getOrDefault("web.api", true);
ADV_API_ENABLED = CONFIG.getOrDefault("web.api.adv", true); ADV_API_ENABLED = CONFIG.getOrDefault("web.api.adv", true);
API_INGAME_COMMAND_ENABLED = CONFIG.getOrDefault("web.api.cmd", true); API_INGAME_COMMAND_ENABLED = CONFIG.getOrDefault("web.api.cmd", true);

View file

@ -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.ApiRequests;
import me.jonasjones.mcwebserver.web.api.ApiRequestsUtil; import me.jonasjones.mcwebserver.web.api.ApiRequestsUtil;
import me.jonasjones.mcwebserver.web.api.v2.ApiV2Handler; import me.jonasjones.mcwebserver.web.api.v2.ApiV2Handler;
import me.jonasjones.mcwebserver.web.api.v2.tokenmgr.TokenManager;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -22,6 +23,7 @@ import java.nio.file.Files;
import java.nio.file.LinkOption; import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import java.time.Instant; import java.time.Instant;
import java.util.StringTokenizer; import java.util.StringTokenizer;
@ -121,8 +123,8 @@ public class HttpServer implements Runnable {
while ((header = in.readLine()) != null && !header.isEmpty()) { while ((header = in.readLine()) != null && !header.isEmpty()) {
// Check if the header contains your API token // Check if the header contains your API token
if (header.startsWith("Authorization: Bearer ")) { if (header.startsWith("Authorization: Basic ")) {
apiToken = header.substring("Authorization: Bearer ".length()); apiToken = header.substring("Authorization: Basic ".length());
} }
} }
@ -238,35 +240,68 @@ public class HttpServer implements Runnable {
fileRequested = fileRequested.substring(1); fileRequested = fileRequested.substring(1);
} }
Path file = WEB_ROOT.resolve(fileRequested).toRealPath(LinkOption.NOFOLLOW_LINKS); try {
if (!file.startsWith(WEB_ROOT)) { boolean isAuthorized = false;
VerboseLogger.warn("Access to file outside root: " + file); if (!ModConfigs.WEB_REQUIRE_TOKEN) {
throw new NoSuchFileException(fileRequested); isAuthorized = true;
} } else if (apiToken != null) {
int fileLength = (int) Files.size(file); isAuthorized = TokenManager.isTokenValid(apiToken);
int fileExtensionStartIndex = fileRequested.lastIndexOf(".") + 1; }
String contentType;
if (fileExtensionStartIndex > 0) { if (isAuthorized) {
contentType = mimetypeidentifier.compare(fileRequested.substring(fileExtensionStartIndex));
} else { Path file = WEB_ROOT.resolve(fileRequested).toRealPath(LinkOption.NOFOLLOW_LINKS);
contentType = "text/plain"; 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");
} }

View file

@ -11,12 +11,16 @@ public class ApiRequests {
return "[\"" + value + "\"]"; return "[\"" + value + "\"]";
} }
public static String playerLookupRequest(String playerName) {
return gson.toJson(ApiRequestsUtil.playerLookup(playerName));
}
public static String playerNamesRequest() { public static String playerNamesRequest() {
return gson.toJsonTree(ApiRequestsUtil.convertPlayerList(ApiRequestsUtil.getSERVER_METADATA().players().get().sample())).getAsJsonArray().toString(); return gson.toJsonTree(ApiRequestsUtil.convertPlayerList(ApiRequestsUtil.getSERVER_METADATA().players().get().sample())).getAsJsonArray().toString();
} }
public static String playerInfoRequest(String playerName) { public static String playerInfoAllRequest(String playerUuid) {
return gson.toJson(ApiRequestsUtil.getPlayerInfo(playerName)); return gson.toJson(ApiRequestsUtil.getPlayerInfo(playerUuid));
} }
public static String serverMetadataRequest() { public static String serverMetadataRequest() {

View file

@ -124,9 +124,18 @@ public class ApiRequestsUtil {
return ApiRequestsUtil.getSERVER_METADATA().favicon().get().iconBytes(); 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()) { for (ServerPlayerEntity player : ApiRequestsUtil.getSERVER_PLAYER_ENTITY_LIST()) {
if (player.getName().getString().equals(playerName)) { 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 { try {
String sleepDirection = (player.getSleepingDirection() != null) ? player.getSleepingDirection().asString() : null; String sleepDirection = (player.getSleepingDirection() != null) ? player.getSleepingDirection().asString() : null;
Vec<Integer> sleepPosition = new Vec<>(); Vec<Integer> sleepPosition = new Vec<>();

View file

@ -13,6 +13,14 @@ public class ErrorHandler {
return gson.toJsonTree(badRequest()).getAsJsonObject().toString(); 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() { public static Error internalServerError() {
return new Error(500, "Internal Server Error"); return new Error(500, "Internal Server Error");
} }

View file

@ -70,11 +70,22 @@ public class ApiV2Handler {
if (isTokenValid) { if (isTokenValid) {
request = request.replace("/api/v2/", ""); request = request.replace("/api/v2/", "");
if (request.startsWith("playerinfo?playername=")) { if (request.startsWith("playerlookup?uuid=")) {
String playerName = request.replace("playerinfo?playername=", ""); return ApiRequests.playerLookupRequest(request.replace("playerlookup?uuid=", ""));
return ApiRequests.playerInfoRequest(playerName); } else if (request.startsWith("playerinfo/inventory?uuid=")) {
} else if (request.startsWith("playerinfo?playeruuid=")) { String playerName = request.replace("playerinfo/inventory?uuid=", "");
return ErrorHandler.badRequestString(); //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 { } else {
return ErrorHandler.notFoundErrorString(); return ErrorHandler.notFoundErrorString();
} }