diff --git a/common/build.gradle.kts b/common/build.gradle.kts index aa1ea84..45be7fa 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation("net.kyori:adventure-api:4.25.0") implementation("net.kyori:adventure-text-minimessage:4.25.0") + implementation("org.slf4j:slf4j-api:2.0.17") } java { diff --git a/common/src/main/java/online/mineroo/common/BindRequest.java b/common/src/main/java/online/mineroo/common/BindRequest.java new file mode 100644 index 0000000..3edce4e --- /dev/null +++ b/common/src/main/java/online/mineroo/common/BindRequest.java @@ -0,0 +1,153 @@ +package online.mineroo.common; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import online.mineroo.common.NetworkServiceInterface; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.concurrent.CompletableFuture; + +/** + * BindRequest handles server binding and verification with the Mineroo API. + *

+ * This class operates asynchronously and relies on the injected + * {@link NetworkService} + * to handle the actual HTTP transport and authentication. + */ +public class BindRequest { + + private final Logger logger; + private final NetworkServiceInterface networkService; + + // State cache + private String motdToken = ""; + private String sessionToken = ""; + + /** + * Constructs a BindRequest instance. + * + * @param logger Logger for logging errors and information + * @param networkService The network service instance (Local or Proxy) that + * handles transport and auth + */ + public BindRequest(Logger logger, NetworkServiceInterface networkService) { + this.logger = logger; + this.networkService = networkService; + } + + /** + * Checks the bind status asynchronously. + * + * @return A future that completes with true if status is 'bound', false + * otherwise + */ + public CompletableFuture checkBindStatus() { + // NetworkService is already configured with BaseURL, only need the path here + String path = "/server/server-bind-status"; + + return networkService.getData(path, null) + .thenApply(responseBody -> { + try { + JsonObject responseData = JsonParser.parseString(responseBody).getAsJsonObject(); + boolean bound = responseData.has("bound") && responseData.get("bound").getAsBoolean(); + + if (!bound) { + logger.warn("Mineroo Bind: Server not bound. 'bound' field is false."); + } + return bound; + } catch (Exception e) { + logger.error("Mineroo Bind: Failed to parse bind status response", e); + return false; + } + }) + .exceptionally(e -> { + logger.error("Mineroo Bind: Network error while checking status", e); + return false; + }); + } + + /** + * Initiates a MOTD verification request asynchronously. + * + * @param hostname The server hostname + * @param port The server port + * @return A future that completes with true if tokens are received + */ + public CompletableFuture initialMotdVerifyRequest(String hostname, Integer port) { + String path = "/server/motd-verify"; + + JsonObject json = new JsonObject(); + json.addProperty("server_address", hostname); + json.addProperty("server_port", port); + + return networkService.postData(path, json) + .thenApply(responseBody -> { + try { + JsonObject responseData = JsonParser.parseString(responseBody).getAsJsonObject(); + + if (responseData.has("motd_token")) { + this.motdToken = responseData.get("motd_token").getAsString(); + } else { + logger.error("Mineroo Bind: MOTD Token not received."); + return false; + } + + if (responseData.has("session_token")) { + this.sessionToken = responseData.get("session_token").getAsString(); + } else { + logger.error("Mineroo Bind: Session Token not received."); + return false; + } + + return true; + + } catch (Exception e) { + logger.error("Mineroo Bind: Failed to parse MOTD verify response: " + responseBody, e); + return false; + } + }) + .exceptionally(e -> { + logger.error("Mineroo Bind: Network error during MOTD verify", e); + return false; + }); + } + + /** + * Finalizes the server binding asynchronously. + * + * @param name The server name (nullable) + * @param description The server description (nullable) + * @return A future that completes with true if the binding is successful + */ + public CompletableFuture finalizeServerBinding(@Nullable String name, @Nullable String description) { + String path = "/server/server-bind"; + + JsonObject json = new JsonObject(); + json.addProperty("session_token", this.sessionToken); + json.addProperty("name", name); + json.addProperty("description", description); + + return networkService.postData(path, json) + .thenApply(responseBody -> { + // Assume as long as the request returns successfully and no exception is + // thrown, it is considered successful (HTTP 200) + // If the API returns a specific {"success": false}, you need to parse the JSON + // and check here + return true; + }) + .exceptionally(e -> { + logger.error("Mineroo Bind: Failed to finalize binding", e); + return false; + }); + } + + /** + * Gets the MOTD token received from the API. + * + * @return the motdToken string + */ + public String getMotdToken() { + return motdToken; + } +} diff --git a/common/src/main/java/online/mineroo/common/HttpNetworkService.java b/common/src/main/java/online/mineroo/common/HttpNetworkService.java new file mode 100644 index 0000000..930ba18 --- /dev/null +++ b/common/src/main/java/online/mineroo/common/HttpNetworkService.java @@ -0,0 +1,93 @@ +package online.mineroo.common; + +import com.google.gson.JsonObject; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class HttpNetworkService implements NetworkServiceInterface { + + private final HttpClient client; + private final String baseUrl; + private final String authHeaderValue; // Precomputed Authorization header value + + /** + * @param baseUrl Base URL of the API (e.g., "https://oapi.mineroo.online") + * @param username Username for Basic Auth (usually "mbind" in your case) + * @param password Password for Basic Auth (the token) + */ + public HttpNetworkService(String baseUrl, String username, String password) { + this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + + // It is recommended to configure connectTimeout + this.client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + // Pre-generate Basic Auth string + String auth = username + ":" + password; + this.authHeaderValue = "Basic " + Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public CompletableFuture getData(String endpoint, Map params) { + return CompletableFuture.supplyAsync(() -> { + try { + // 1. Build URL and query parameters + StringBuilder urlBuilder = new StringBuilder(baseUrl + endpoint); + if (params != null && !params.isEmpty()) { + urlBuilder.append("?"); + params.forEach((k, v) -> urlBuilder.append(k).append("=").append(v).append("&")); + // Simple trailing '&' removal (usually harmless, but cleaner) + urlBuilder.setLength(urlBuilder.length() - 1); + } + + // 2. Build the request + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(urlBuilder.toString())) + .header("Authorization", authHeaderValue) // Inject Auth + .header("User-Agent", "Mineroo-Plugin/1.0") // Recommended to add User-Agent + .GET() + .build(); + + // 3. Send the request + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + // You can do a simple status code check here, or leave it to the upper layer + // To keep the interface pure, we just return the body + return response.body(); + + } catch (Exception e) { + // Wrap checked exceptions as runtime exceptions, Future will catch them + throw new RuntimeException("HTTP GET failed: " + endpoint, e); + } + }); + } + + @Override + public CompletableFuture postData(String endpoint, JsonObject body) { + return CompletableFuture.supplyAsync(() -> { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + endpoint)) + .header("Authorization", authHeaderValue) // Inject Auth + .header("Content-Type", "application/json") + .header("User-Agent", "Mineroo-Plugin/1.0") + .POST(HttpRequest.BodyPublishers.ofString(body.toString(), StandardCharsets.UTF_8)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + return response.body(); + + } catch (Exception e) { + throw new RuntimeException("HTTP POST failed: " + endpoint, e); + } + }); + } +} diff --git a/common/src/main/java/online/mineroo/common/NetworkServiceInterface.java b/common/src/main/java/online/mineroo/common/NetworkServiceInterface.java new file mode 100644 index 0000000..9823824 --- /dev/null +++ b/common/src/main/java/online/mineroo/common/NetworkServiceInterface.java @@ -0,0 +1,12 @@ +package online.mineroo.common; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.google.gson.JsonObject; + +public interface NetworkServiceInterface { + CompletableFuture getData(String endpoint, Map params); + + CompletableFuture postData(String endpoint, JsonObject body); +} diff --git a/kls_database.db b/kls_database.db index ffd8076..f26bd3f 100644 Binary files a/kls_database.db and b/kls_database.db differ diff --git a/velocity/src/main/java/online/mineroo/velocity/commands/MainCommand.java b/velocity/src/main/java/online/mineroo/velocity/commands/MainCommand.java index 7379db4..bbf1e1c 100644 --- a/velocity/src/main/java/online/mineroo/velocity/commands/MainCommand.java +++ b/velocity/src/main/java/online/mineroo/velocity/commands/MainCommand.java @@ -1,6 +1,7 @@ package online.mineroo.velocity.commands; import java.util.List; +import java.util.concurrent.CompletionException; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -12,9 +13,11 @@ import com.velocitypowered.api.command.SimpleCommand; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import online.mineroo.common.MessageManager; +import online.mineroo.common.NetworkServiceInterface; import online.mineroo.velocity.Config; import online.mineroo.velocity.MinerooCore; -import online.mineroo.velocity.utils.BindRequest; +import online.mineroo.common.BindRequest; +import online.mineroo.common.HttpNetworkService; public class MainCommand implements SimpleCommand { @@ -94,43 +97,64 @@ public class MainCommand implements SimpleCommand { Logger logger = plugin.getLogger(); + // Run in a separate worker thread to allow blocking (Thread.sleep) plugin.getServer().getScheduler().buildTask(plugin, () -> { - BindRequest request = new BindRequest(logger, bind_token); + // 1. Initialize the Network Service locally (Velocity connects directly) + NetworkServiceInterface networkService = new HttpNetworkService( + "https://oapi.mineroo.online", + "mbind", + bind_token); + + BindRequest request = new BindRequest(logger, networkService); try { - // Status check before initial bind - if (request.checkBindStatus()) { + // 2. Check Status + // We use .join() to block the thread until the Future completes. + if (request.checkBindStatus().join()) { source.sendMessage(msg.get("command.bind.server.already-bound")); return; } - boolean inital_bind = request.initalMotdVerifyRequest(bind_address, bind_port); + // 3. Initial Verification + boolean inital_bind = request.initialMotdVerifyRequest(bind_address, bind_port).join(); + if (inital_bind) { String motdToken = request.getMotdToken(); plugin.getBindListener().setVerificationToken(motdToken); source.sendMessage(msg.get("command.bind.server.wait")); - Thread.sleep(2 * 60 * 1000 + 5000); + // 4. Wait for external refresh (Blocking this worker thread is intended) + try { + Thread.sleep(2 * 60 * 1000 + 5000); // 2m 5s + } catch (InterruptedException e) { + logger.error("Bind thread interrupted", e); + return; + } - // Final Bind + // 5. Final Bind & Retry Loop boolean success = false; int maxRetries = 3; for (int i = 1; i <= maxRetries; i++) { - if (request.finalizeServerBinding(bind_name, bind_description)) { + // Check final status synchronously using join() + if (request.finalizeServerBinding(bind_name, bind_description).join()) { success = true; break; } if (i < maxRetries) { - source.sendMessage(msg.get("command.bind.server.retry", "current", String.valueOf(i), "max", String.valueOf(maxRetries))); - Thread.sleep(10000); + + try { + Thread.sleep(10000); // Wait 10s before retry + } catch (InterruptedException e) { + break; + } } } @@ -145,14 +169,16 @@ public class MainCommand implements SimpleCommand { Component.text("Initial handshake failed. Check your token or network.", NamedTextColor.RED)); } + } catch (CompletionException e) { + // Unpack the wrapper exception from join() to see the real network error + logger.error("Mineroo Bind Network Error: " + e.getCause().getMessage()); } catch (Exception e) { - logger.error("Mineroo Bind: " + e.toString()); + logger.error("Mineroo Bind Error: " + e.toString()); } finally { plugin.getBindListener().clearVerificationToken(); } }).schedule(); - } public void sendHelp(CommandSource source) { diff --git a/velocity/src/main/java/online/mineroo/velocity/commands/NetworkService.java b/velocity/src/main/java/online/mineroo/velocity/commands/NetworkService.java new file mode 100644 index 0000000..e69de29 diff --git a/velocity/src/main/java/online/mineroo/velocity/utils/BindRequest.java b/velocity/src/main/java/online/mineroo/velocity/utils/BindRequest.java deleted file mode 100644 index 7d699b8..0000000 --- a/velocity/src/main/java/online/mineroo/velocity/utils/BindRequest.java +++ /dev/null @@ -1,205 +0,0 @@ -package online.mineroo.velocity.utils; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; - -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; - -/** - * BindRequest handles server binding and verification with the Mineroo API. - *

- * This class provides methods to: - *

- * It manages authentication, HTTP requests, and response parsing for the binding process. - */ -public class BindRequest { - - /** - * The base endpoint for Mineroo API requests. - */ - private static final String endpoint = "https://oapi.mineroo.online"; - - /** - * Logger instance for logging errors and information. - */ - private final Logger logger; - /** - * HTTP client used for sending requests to the Mineroo API. - */ - private final HttpClient httpClient; - - /** - * The bind token used for authenticating API requests. - */ - private final String bindToken; - - /** - * The MOTD token received from the API after verification. - */ - private String motdToken = ""; - /** - * The session token received from the API after verification. - */ - private String sessionToken = ""; - - /** - * Constructs a BindRequest instance. - * - * @param logger Logger for logging errors and information - * @param bindToken The bind token for API authentication - */ - public BindRequest(Logger logger, String bindToken) { - - this.logger = logger; - this.bindToken = bindToken; - - this.httpClient = HttpClient.newHttpClient(); - } - - /** - * Checks the bind status from the oapi.mineroo.online API. - * - * @return true if status is 'bound', false otherwise - */ - /** - * Checks the bind status from the oapi.mineroo.online API. - * - * @return true if status is 'bound', false otherwise - */ - public boolean checkBindStatus() { - String uri = endpoint + "/server/server-bind-status"; - String basicAuth = "mbind:" + bindToken; - String encodedAuth = Base64.getEncoder().encodeToString(basicAuth.getBytes(StandardCharsets.UTF_8)); - try { - HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) - .header("Authorization", "Basic " + encodedAuth) - .GET().build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() == 200) { - JsonObject responseData = JsonParser.parseString(response.body()).getAsJsonObject(); - boolean bound = responseData.has("bound") && responseData.get("bound").getAsBoolean(); - if (bound) { - return true; - } else { - logger.error("Mineroo Bind: Server not bound. 'bound' field is false."); - } - } else { - logger.error("Mineroo Bind: Failed to check bind status. Response: " + response.body()); - } - } catch (Exception e) { - logger.error("Mineroo Bind: Exception while checking bind status", e); - } - return false; - } - - /** - * Initiates a MOTD verification request to the Mineroo API. - * - * @param hostname The server hostname - * @param port The server port - * @return true if both motd_token and session_token are received, false otherwise - * @throws IOException, InterruptedException, JsonParseException on request or parsing failure - */ - public boolean initalMotdVerifyRequest(String hostname, Integer port) - throws IOException, InterruptedException, JsonParseException { - - // generate uri - String uri = endpoint + "/server/motd-verify"; - - // build jsonBody - JsonObject json = new JsonObject(); - json.addProperty("server_address", hostname); - json.addProperty("server_port", port); - - String basicAuth = "mbind:" + bindToken; - String encodedAuth = Base64.getEncoder().encodeToString(basicAuth.getBytes(StandardCharsets.UTF_8)); - - HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) - .header("Content-Type", "application/json") - .header("Authorization", "Basic " + encodedAuth) - .POST(HttpRequest.BodyPublishers.ofString(json.toString())).build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() == 200) { - String body = response.body(); - JsonObject responseData = JsonParser.parseString(body).getAsJsonObject(); - - if (responseData.has("motd_token")) { - motdToken = responseData.get("motd_token").getAsString(); - } else { - logger.error("Mineroo Bind: MOTD Token not received."); - return false; - } - - if (responseData.has("session_token")) { - sessionToken = responseData.get("session_token").getAsString(); - } else { - logger.error("Mineroo Bind: Session Token not received."); - return false; - } - - return true; - - } else { - logger.error("Mineroo Bind: " + response.body().toString()); - } - - return false; - } - - /** - * Finalizes the server binding by sending server details to the Mineroo API. - * - * @param name The server name (nullable) - * @param description The server description (nullable) - * @return true if the binding is successful (HTTP 200), false otherwise - * @throws IOException, InterruptedException, JsonParseException on request or parsing failure - */ - public boolean finalizeServerBinding(@Nullable String name, @Nullable String description) - throws IOException, InterruptedException, JsonParseException { - - String uri = endpoint + "/server/server-bind"; - - JsonObject json = new JsonObject(); - json.addProperty("session_token", sessionToken); - json.addProperty("name", name); - json.addProperty("description", description); - - String basicAuth = "mbind:" + bindToken; - String encodedAuth = Base64.getEncoder().encodeToString(basicAuth.getBytes(StandardCharsets.UTF_8)); - - HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) - .header("Content-Type", "application/json") - .header("Authorization", "Basic " + encodedAuth) - .POST(HttpRequest.BodyPublishers.ofString(json.toString())).build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - return response.statusCode() == 200; - - } - - /** - * Gets the MOTD token received from the API. - * - * @return the motdToken string - */ - public String getMotdToken() { - return motdToken; - } -}