From d54c14f66afc101b7252652e88f66ece19a787e0 Mon Sep 17 00:00:00 2001 From: XiaoMoMi Date: Thu, 17 Apr 2025 02:45:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=B8=80=E6=AC=A1=E6=80=A7to?= =?UTF-8?q?ken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/pack/AbstractPackManager.java | 1 - .../pack/host/ResourcePackDownloadData.java | 4 + .../core/pack/host/impl/SelfHost.java | 12 +- .../pack/host/impl/SelfHostHttpServer.java | 338 ++++++++++++------ .../pack/host/impl/SimpleExternalHost.java | 6 + 5 files changed, 239 insertions(+), 122 deletions(-) diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java b/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java index b223f2e11..d0fe4351f 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java @@ -143,7 +143,6 @@ public abstract class AbstractPackManager implements PackManager { @Override public void load() { - this.calculateHash(); } @Override diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/host/ResourcePackDownloadData.java b/core/src/main/java/net/momirealms/craftengine/core/pack/host/ResourcePackDownloadData.java index 139b42814..9a39f1d70 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/pack/host/ResourcePackDownloadData.java +++ b/core/src/main/java/net/momirealms/craftengine/core/pack/host/ResourcePackDownloadData.java @@ -3,4 +3,8 @@ package net.momirealms.craftengine.core.pack.host; import java.util.UUID; public record ResourcePackDownloadData(String url, UUID uuid, String sha1) { + + public static ResourcePackDownloadData of(String url, UUID uuid, String sha1) { + return new ResourcePackDownloadData(url, uuid, sha1); + } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHost.java b/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHost.java index 5b8880e02..32f117c07 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHost.java +++ b/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHost.java @@ -9,18 +9,24 @@ import java.util.concurrent.CompletableFuture; public class SelfHost implements ResourcePackHost { + public SelfHost(String ip, int port) { + SelfHostHttpServer.instance().setIp(ip); + SelfHostHttpServer.instance().updatePort(port); + } + @Override public CompletableFuture requestResourcePackDownloadLink(UUID player) { - return null; + return CompletableFuture.completedFuture(SelfHostHttpServer.instance().generateOneTimeUrl(player)); } @Override public ResourcePackDownloadData getResourcePackDownloadLink(UUID player) { - return null; + return SelfHostHttpServer.instance().getCachedOneTimeUrl(player); } @Override public CompletableFuture upload(Path resourcePackPath) { - return null; + SelfHostHttpServer.instance().setResourcePackPath(resourcePackPath); + return CompletableFuture.completedFuture(true); } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHostHttpServer.java b/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHostHttpServer.java index 2c05ed049..4426f1e3f 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHostHttpServer.java +++ b/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHostHttpServer.java @@ -5,40 +5,82 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; +import net.momirealms.craftengine.core.pack.host.ResourcePackDownloadData; import net.momirealms.craftengine.core.plugin.CraftEngine; import net.momirealms.craftengine.core.plugin.config.Config; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; public class SelfHostHttpServer { private static SelfHostHttpServer instance; - private Cache oneTimePackUrls = Caffeine.newBuilder() + private final Cache oneTimePackUrls = Caffeine.newBuilder() + .maximumSize(256) .expireAfterAccess(1, TimeUnit.MINUTES) .build(); + private final Cache playerTokens = Caffeine.newBuilder() + .maximumSize(256) + .expireAfterAccess(1, TimeUnit.MINUTES) + .build(); + private final Cache ipAccessCache = Caffeine.newBuilder() + .maximumSize(256) + .expireAfterAccess(10, TimeUnit.MINUTES) + .build(); + + private final ExecutorService threadPool = Executors.newFixedThreadPool(1); private HttpServer server; - private final ConcurrentHashMap ipAccessMap = new ConcurrentHashMap<>(); + + private final AtomicLong totalRequests = new AtomicLong(); + private final AtomicLong blockedRequests = new AtomicLong(); + private int rateLimit = 1; private long rateLimitInterval = 1000; private String ip = "localhost"; private int port = -1; - private Path resourcePackPath; + + private volatile byte[] resourcePackBytes; private String packHash; private UUID packUUID; - public String generateOneTimeUrl(UUID player) { + public void setIp(String ip) { + this.ip = ip; + } + @NotNull + public ResourcePackDownloadData generateOneTimeUrl(UUID player) { + String token = UUID.randomUUID().toString(); + this.oneTimePackUrls.put(token, true); + this.playerTokens.put(player, token); + return new ResourcePackDownloadData( + url() + "download?token=" + URLEncoder.encode(token, StandardCharsets.UTF_8), + packUUID, + packHash + ); + } + + @Nullable + public ResourcePackDownloadData getCachedOneTimeUrl(UUID player) { + String token = this.playerTokens.getIfPresent(player); + if (token == null) return null; + return new ResourcePackDownloadData( + url() + "download?token=" + URLEncoder.encode(token, StandardCharsets.UTF_8), + packUUID, + packHash + ); } public String url() { @@ -46,51 +88,200 @@ public class SelfHostHttpServer { } public void setResourcePackPath(Path resourcePackPath) { - this.resourcePackPath = resourcePackPath; + try { + if (Files.exists(resourcePackPath)) { + this.resourcePackBytes = Files.readAllBytes(resourcePackPath); + calculateHash(); + } else { + CraftEngine.instance().logger().warn("Resource pack file not found: " + resourcePackPath); + } + } catch (IOException e) { + CraftEngine.instance().logger().severe("Failed to load resource pack", e); + } } private void calculateHash() { - if (Files.exists(this.resourcePackPath)) { - try { - this.packHash = computeSHA1(this.resourcePackPath); - this.packUUID = UUID.nameUUIDFromBytes(this.packHash.getBytes(StandardCharsets.UTF_8)); - } catch (IOException | NoSuchAlgorithmException e) { - CraftEngine.instance().logger().severe("Error calculating resource pack hash", e); + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.update(resourcePackBytes); + byte[] hashBytes = digest.digest(); + + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + hexString.append(String.format("%02x", b)); } - } else { - this.packHash = ""; - this.packUUID = UUID.nameUUIDFromBytes("EMPTY".getBytes(StandardCharsets.UTF_8)); + this.packHash = hexString.toString(); + this.packUUID = UUID.nameUUIDFromBytes(packHash.getBytes(StandardCharsets.UTF_8)); + } catch (NoSuchAlgorithmException e) { + CraftEngine.instance().logger().severe("SHA-1 algorithm not available", e); } } public void updatePort(int port) { - if (port == this.port) { - return; - } - if (server != null) { - disable(); - } + if (port == this.port) return; + if (server != null) disable(); this.port = port; try { server = HttpServer.create(new InetSocketAddress("::", port), 0); - server.createContext("/", new ResourcePackHandler()); - server.setExecutor(Executors.newCachedThreadPool()); + server.createContext("/download", new ResourcePackHandler()); + server.createContext("/metrics", this::handleMetrics); + server.setExecutor(threadPool); server.start(); - CraftEngine.instance().logger().info("HTTP resource pack server running on port: " + port); + CraftEngine.instance().logger().info("HTTP server started on port: " + port); } catch (IOException e) { CraftEngine.instance().logger().warn("Failed to start HTTP server", e); } } + private void handleMetrics(HttpExchange exchange) throws IOException { + String metrics = "# TYPE total_requests counter\n" + + "total_requests " + totalRequests.get() + "\n" + + "# TYPE blocked_requests counter\n" + + "blocked_requests " + blockedRequests.get(); + + exchange.getResponseHeaders().set("Content-Type", "text/plain"); + exchange.sendResponseHeaders(200, metrics.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(metrics.getBytes(StandardCharsets.UTF_8)); + } + } + public void disable() { if (server != null) { server.stop(0); server = null; + threadPool.shutdownNow(); } } - public boolean isAlive() { - return server != null; + public void adjustRateLimit(int requestsPerSecond, int rateLimitInterval) { + this.rateLimit = requestsPerSecond; + this.rateLimitInterval = rateLimitInterval; + CraftEngine.instance().logger().info("Updated rate limit to " + requestsPerSecond + "/s"); + } + + private class ResourcePackHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + totalRequests.incrementAndGet(); + + String clientIp = getClientIp(exchange); + + if (checkRateLimit(clientIp)) { + handleBlockedRequest(exchange, 429, "Rate limit exceeded"); + return; + } + + String token = parseToken(exchange); + if (!validateToken(token)) { + handleBlockedRequest(exchange, 403, "Invalid token"); + return; + } + + if (!validateClient(exchange)) { + handleBlockedRequest(exchange, 403, "Invalid client"); + return; + } + + if (resourcePackBytes == null) { + handleBlockedRequest(exchange, 404, "Resource pack missing"); + return; + } + + sendResourcePack(exchange); + } + + private String getClientIp(HttpExchange exchange) { + return exchange.getRemoteAddress().getAddress().getHostAddress(); + } + + private boolean checkRateLimit(String clientIp) { + IpAccessRecord record = ipAccessCache.getIfPresent(clientIp); + long now = System.currentTimeMillis(); + if (record == null) { + record = new IpAccessRecord(now, 1); + ipAccessCache.put(clientIp, record); + } else { + if (now - record.lastAccessTime > rateLimitInterval) { + record = new IpAccessRecord(now, 1); + ipAccessCache.put(clientIp, record); + } else { + record.accessCount++; + } + } + return record.accessCount > rateLimit; + } + + private String parseToken(HttpExchange exchange) { + Map params = parseQuery(exchange.getRequestURI().getQuery()); + return params.get("token"); + } + + private boolean validateToken(String token) { + if (token == null || token.length() != 36) return false; + + Boolean valid = oneTimePackUrls.getIfPresent(token); + if (valid != null) { + oneTimePackUrls.invalidate(token); + return true; + } + return false; + } + + private boolean validateClient(HttpExchange exchange) { + if (!Config.denyNonMinecraftRequest()) return true; + + String userAgent = exchange.getRequestHeaders().getFirst("User-Agent"); + return userAgent != null && userAgent.startsWith("Minecraft Java/"); + } + + private void sendResourcePack(HttpExchange exchange) throws IOException { + exchange.getResponseHeaders().set("Content-Type", "application/zip"); + exchange.getResponseHeaders().set("Content-Length", String.valueOf(resourcePackBytes.length)); + exchange.sendResponseHeaders(200, resourcePackBytes.length); + + try (OutputStream os = exchange.getResponseBody()) { + os.write(resourcePackBytes); + } catch (IOException e) { + CraftEngine.instance().logger().warn("Failed to send resource pack", e); + throw e; + } + } + + private void handleBlockedRequest(HttpExchange exchange, int code, String reason) throws IOException { + blockedRequests.incrementAndGet(); + CraftEngine.instance().debug(() -> + String.format("Blocked request [%s] %s: %s", + code, + exchange.getRemoteAddress(), + reason) + ); + exchange.sendResponseHeaders(code, -1); + exchange.close(); + } + + private Map parseQuery(String query) { + Map params = new HashMap<>(); + if (query == null) return params; + + for (String pair : query.split("&")) { + int idx = pair.indexOf("="); + String key = idx > 0 ? pair.substring(0, idx) : pair; + String value = idx > 0 ? pair.substring(idx + 1) : ""; + params.put(key, value); + } + return params; + } + } + + private static class IpAccessRecord { + final long lastAccessTime; + int accessCount; + + IpAccessRecord(long lastAccessTime, int accessCount) { + this.lastAccessTime = lastAccessTime; + this.accessCount = accessCount; + } } public static SelfHostHttpServer instance() { @@ -99,93 +290,4 @@ public class SelfHostHttpServer { } return instance; } - - public void setRateLimit(int rateLimit, long rateLimitInterval, TimeUnit timeUnit) { - this.rateLimit = rateLimit; - this.rateLimitInterval = timeUnit.toMillis(rateLimitInterval); - } - - public void setIp(String ip) { - this.ip = ip; - } - - private String computeSHA1(Path path) throws IOException, NoSuchAlgorithmException { - InputStream file = Files.newInputStream(path); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = file.read(buffer)) != -1) { - digest.update(buffer, 0, bytesRead); - } - file.close(); - - StringBuilder hexString = new StringBuilder(40); - for (byte b : digest.digest()) { - hexString.append(String.format("%02x", b)); - } - return hexString.toString(); - } - - private class ResourcePackHandler implements HttpHandler { - @Override - public void handle(HttpExchange exchange) throws IOException { - if (Config.denyNonMinecraftRequest()) { - String userAgent = exchange.getRequestHeaders().getFirst("User-Agent"); - if (userAgent == null || !userAgent.startsWith("Minecraft Java/")) { - CraftEngine.instance().debug(() -> "Blocked non-Minecraft Java client. User-Agent: " + userAgent); - sendError(exchange, 403); - return; - } - } - - String clientIp = exchange.getRemoteAddress().getAddress().getHostAddress(); - - IpAccessRecord record = ipAccessMap.compute(clientIp, (k, v) -> { - long currentTime = System.currentTimeMillis(); - if (v == null || currentTime - v.lastAccessTime > rateLimitInterval) { - return new IpAccessRecord(currentTime, 1); - } else { - v.accessCount++; - return v; - } - }); - - if (record.accessCount > rateLimit) { - CraftEngine.instance().debug(() -> "Rate limit exceeded for IP: " + clientIp); - sendError(exchange, 429); - return; - } - - if (!Files.exists(resourcePackPath)) { - CraftEngine.instance().logger().warn("ResourcePack not found: " + resourcePackPath); - sendError(exchange, 404); - return; - } - - exchange.getResponseHeaders().set("Content-Type", "application/zip"); - exchange.getResponseHeaders().set("Content-Length", String.valueOf(Files.size(resourcePackPath))); - exchange.sendResponseHeaders(200, Files.size(resourcePackPath)); - - try (OutputStream os = exchange.getResponseBody()) { - Files.copy(resourcePackPath, os); - } catch (IOException e) { - CraftEngine.instance().logger().warn("Failed to send pack", e); - } - } - - private void sendError(HttpExchange exchange, int code) throws IOException { - exchange.sendResponseHeaders(code, 0); - exchange.getResponseBody().close(); - } - } - - private static class IpAccessRecord { - long lastAccessTime; - int accessCount; - - IpAccessRecord(long lastAccessTime, int accessCount) { - this.lastAccessTime = lastAccessTime; - this.accessCount = accessCount; - } - } } \ No newline at end of file diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SimpleExternalHost.java b/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SimpleExternalHost.java index a021836bd..4c736cfb1 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SimpleExternalHost.java +++ b/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SimpleExternalHost.java @@ -3,6 +3,7 @@ package net.momirealms.craftengine.core.pack.host.impl; import net.momirealms.craftengine.core.pack.host.ResourcePackDownloadData; import net.momirealms.craftengine.core.pack.host.ResourcePackHost; +import java.nio.file.Path; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -22,4 +23,9 @@ public class SimpleExternalHost implements ResourcePackHost { public ResourcePackDownloadData getResourcePackDownloadLink(UUID player) { return this.downloadData; } + + @Override + public CompletableFuture upload(Path resourcePackPath) { + return CompletableFuture.completedFuture(true); + } }