diff --git a/ServerStatus/src/main/kotlin/co/caadiq/serverstatus/ServerStatusMod.kt b/ServerStatus/src/main/kotlin/co/caadiq/serverstatus/ServerStatusMod.kt index 4e989b3..d50c107 100644 --- a/ServerStatus/src/main/kotlin/co/caadiq/serverstatus/ServerStatusMod.kt +++ b/ServerStatus/src/main/kotlin/co/caadiq/serverstatus/ServerStatusMod.kt @@ -8,6 +8,7 @@ import co.caadiq.serverstatus.data.PlayerTracker import co.caadiq.serverstatus.data.ServerDataCollector import co.caadiq.serverstatus.data.WorldDataCollector import co.caadiq.serverstatus.network.HttpApiServer +import net.minecraft.server.MinecraftServer import net.neoforged.bus.api.IEventBus import net.neoforged.bus.api.SubscribeEvent import net.neoforged.fml.ModContainer @@ -15,6 +16,8 @@ import net.neoforged.fml.common.Mod import net.neoforged.fml.event.lifecycle.FMLDedicatedServerSetupEvent import net.neoforged.neoforge.common.NeoForge import net.neoforged.neoforge.event.RegisterCommandsEvent +import net.neoforged.neoforge.event.server.ServerStartedEvent +import net.neoforged.neoforge.event.server.ServerStoppedEvent import org.apache.logging.log4j.LogManager /** 메인 모드 클래스 서버 상태 정보를 HTTP API로 제공 */ @@ -37,6 +40,10 @@ class ServerStatusMod(modBus: IEventBus, container: ModContainer) { private set lateinit var httpApiServer: HttpApiServer private set + + // 마인크래프트 서버 인스턴스 (명령어 실행에 사용) + var minecraftServer: MinecraftServer? = null + private set } init { @@ -62,6 +69,20 @@ class ServerStatusMod(modBus: IEventBus, container: ModContainer) { AuthCommand.register(event.dispatcher) } + /** 서버 시작 완료 이벤트 */ + @SubscribeEvent + fun onServerStarted(event: ServerStartedEvent) { + minecraftServer = event.server + LOGGER.info("[$MOD_ID] 마인크래프트 서버 인스턴스 저장됨") + } + + /** 서버 종료 이벤트 */ + @SubscribeEvent + fun onServerStopped(event: ServerStoppedEvent) { + minecraftServer = null + LOGGER.info("[$MOD_ID] 마인크래프트 서버 인스턴스 해제됨") + } + /** 전용 서버 설정 이벤트 */ private fun onServerSetup(event: FMLDedicatedServerSetupEvent) { LOGGER.info("[$MOD_ID] 서버 설정 중...") diff --git a/ServerStatus/src/main/kotlin/co/caadiq/serverstatus/network/HttpApiServer.kt b/ServerStatus/src/main/kotlin/co/caadiq/serverstatus/network/HttpApiServer.kt index 4e75c2e..c0f5fea 100644 --- a/ServerStatus/src/main/kotlin/co/caadiq/serverstatus/network/HttpApiServer.kt +++ b/ServerStatus/src/main/kotlin/co/caadiq/serverstatus/network/HttpApiServer.kt @@ -34,6 +34,7 @@ class HttpApiServer(private val port: Int) { server?.createContext("/players", PlayersHandler()) server?.createContext("/player", PlayerHandler()) server?.createContext("/worlds", WorldsHandler()) + server?.createContext("/command", CommandHandler()) server?.start() ServerStatusMod.LOGGER.info( @@ -53,8 +54,8 @@ class HttpApiServer(private val port: Int) { private fun sendJsonResponse(exchange: HttpExchange, response: String, statusCode: Int = 200) { exchange.responseHeaders.add("Content-Type", "application/json; charset=utf-8") exchange.responseHeaders.add("Access-Control-Allow-Origin", "*") - exchange.responseHeaders.add("Access-Control-Allow-Methods", "GET, OPTIONS") - exchange.responseHeaders.add("Access-Control-Allow-Headers", "Content-Type") + exchange.responseHeaders.add("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + exchange.responseHeaders.add("Access-Control-Allow-Headers", "Content-Type, Authorization") val bytes = response.toByteArray(StandardCharsets.UTF_8) exchange.sendResponseHeaders(statusCode, bytes.size.toLong()) @@ -206,6 +207,74 @@ class HttpApiServer(private val port: Int) { } } } + + /** POST /command - 서버 명령어 실행 */ + private inner class CommandHandler : HttpHandler { + override fun handle(exchange: HttpExchange) { + if (exchange.requestMethod == "OPTIONS") { + sendJsonResponse(exchange, "", 204) + return + } + + if (exchange.requestMethod != "POST") { + sendJsonResponse(exchange, """{"error": "Method not allowed"}""", 405) + return + } + + try { + val body = exchange.requestBody.bufferedReader().readText() + val request = json.decodeFromString(body) + val command = request.command.trim() + + if (command.isEmpty()) { + sendJsonResponse( + exchange, + """{"success": false, "message": "명령어가 비어있습니다"}""", + 400 + ) + return + } + + // 메인 스레드에서 명령어 실행 + val server = ServerStatusMod.minecraftServer + if (server == null) { + sendJsonResponse( + exchange, + """{"success": false, "message": "서버가 실행 중이 아닙니다"}""", + 503 + ) + return + } + + // 슬래시 제거 (있으면) + val cleanCommand = if (command.startsWith("/")) command.substring(1) else command + + server.execute { + try { + val commandSource = server.createCommandSourceStack() + server.commands.performPrefixedCommand(commandSource, cleanCommand) + ServerStatusMod.LOGGER.info("[Admin] 명령어 실행: $cleanCommand") + } catch (e: Exception) { + ServerStatusMod.LOGGER.error("[Admin] 명령어 실행 실패: ${e.message}") + } + } + + sendJsonResponse( + exchange, + """{"success": true, "message": "명령어가 실행되었습니다: $cleanCommand"}""" + ) + } catch (e: Exception) { + ServerStatusMod.LOGGER.error("명령어 실행 오류: ${e.message}") + sendJsonResponse( + exchange, + """{"success": false, "message": "명령어 실행 중 오류 발생"}""", + 500 + ) + } + } + } } @Serializable data class WorldsResponse(val worlds: List) + +@Serializable data class CommandRequest(val command: String)