Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ public void registerEndpoints() {
server.addRoute(NanoHTTPD.Method.POST, "/v1/server/exec", serverAPI::execCommand);
Logger.debug("Registered POST /v1/server/exec");

server.addRoute(NanoHTTPD.Method.POST, "/v1/server/exec-multiple", serverAPI::execMultipleCommands);
Logger.debug("Registered POST /v1/server/exec-multiple");

server.addRoute(NanoHTTPD.Method.POST, "/v1/server/reload", serverAPI::reload);
Logger.debug("Registered POST /v1/server/reload");

Expand Down
85 changes: 85 additions & 0 deletions src/main/java/com/shweit/serverapi/endpoints/v1/ServerAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import org.json.JSONArray;
import org.json.JSONObject;

import java.io.File;
Expand All @@ -24,6 +25,7 @@
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
Expand Down Expand Up @@ -249,6 +251,89 @@ public NanoHTTPD.Response execCommand(final Map<String, String> params) {
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "application/json", jsonResponse.toString());
}

public NanoHTTPD.Response execMultipleCommands(final Map<String, String> params) {
String commandsParam = params.get("commands");
if (commandsParam == null) {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"application/json",
"{\"error\":\"Missing 'commands' parameter\"}"
);
}

JSONArray commandsJsonArray;
try {
commandsJsonArray = new JSONArray(commandsParam);
} catch (Exception e) {
Logger.error("Error parsing 'commands' array: " + e.getMessage());
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"application/json",
"{\"error\":\"'commands' parameter must be a valid JSON array\"}"
);
}

JSONArray resultsArray = new JSONArray();

for (int i = 0; i < commandsJsonArray.length(); i++) {
String command = commandsJsonArray.optString(i);
if (command == null || command.trim().isEmpty()) {
JSONObject result = new JSONObject();
result.put("command", JSONObject.NULL);
result.put("success", false);
result.put("output", "Empty command string provided.");
resultsArray.put(result);
continue;
}

AtomicBoolean success = new AtomicBoolean(false);
CommandOutputCapture outputCapture = new CommandOutputCapture();
JSONObject result = new JSONObject();
result.put("command", command);

try {
BukkitTask task = Bukkit.getScheduler().runTask(MinecraftServerAPI.getInstance(), () -> {
success.set(Bukkit.getServer().dispatchCommand(outputCapture, command));
});

while (!task.isCancelled() && (Bukkit.getScheduler().isCurrentlyRunning(task.getTaskId()) || Bukkit.getScheduler().isQueued(task.getTaskId()))) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Logger.error("Command execution interrupted: " + command);
result.put("success", false);
result.put("output", "Command execution was interrupted.");
break;
}
}

if (Thread.currentThread().isInterrupted()){
Logger.error("Command execution interrupted: " + command);
result.put("success", false);
result.put("output", "Command execution was interrupted.");
}

} catch (Exception e) {
Logger.error("Error executing command '" + command + "': " + e.getMessage());
result.put("success", false);
result.put("output", "Error executing command: " + e.getMessage());
}

if (!result.has("success")) {
result.put("success", success.get());
result.put("output", outputCapture.getOutputMessages());
}
resultsArray.put(result);
}

JSONObject responseJson = new JSONObject();
responseJson.put("results", resultsArray);

return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "application/json", responseJson.toString());
}


public NanoHTTPD.Response reload(final Map<String, String> ignoredParams) {
NanoHTTPD.Response response = NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "application/json", "{}");

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ openapi: 3.0.0
info:
title: Minecraft Server API
description: This API provides endpoints for interacting with and managing
various aspects of a Minecraft server. You can find a public list of available Endpoints [here](https://msa.shweit.com).
various aspects of a Minecraft server. The API specification can be found in the [api.yaml](api.yaml) file.
version: 1.21.x
contact:
name: Dennis van den Brock
Expand Down
8 changes: 5 additions & 3 deletions src/test/java/ServerAPITest.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import io.swagger.v3.core.util.Json;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.net.HttpURLConnection;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertNotNull;

public class ServerAPITest extends ApiTestHelper {
@Test
Expand Down