diff --git a/common-files/src/main/resources/config.yml b/common-files/src/main/resources/config.yml index d2a376c04..50f324641 100644 --- a/common-files/src/main/resources/config.yml +++ b/common-files/src/main/resources/config.yml @@ -197,6 +197,8 @@ resource-pack: deny-non-minecraft-request: true # Generates a single-use, time-limited download link for each player. one-time-token: true + # Enhances validation for deny-non-minecraft-request and one-time-token. + strict-validation: true rate-limiting: # Maximum bandwidth per second to prevent server instability for other players during resource pack downloads max-bandwidth-per-second: 5_000_000 # 5MB/s 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 7d8937de1..93cede575 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 @@ -28,7 +28,7 @@ public class SelfHost implements ResourcePackHost { @Override public CompletableFuture> requestResourcePackDownloadLink(UUID player) { - ResourcePackDownloadData data = SelfHostHttpServer.instance().generateOneTimeUrl(); + ResourcePackDownloadData data = SelfHostHttpServer.instance().generateOneTimeUrl(player); if (data == null) return CompletableFuture.completedFuture(List.of()); return CompletableFuture.completedFuture(List.of(data)); } @@ -77,7 +77,7 @@ public class SelfHost implements ResourcePackHost { boolean oneTimeToken = ResourceConfigUtils.getAsBoolean(arguments.getOrDefault("one-time-token", true), "one-time-token"); String protocol = arguments.getOrDefault("protocol", "http").toString(); boolean denyNonMinecraftRequest = ResourceConfigUtils.getAsBoolean(arguments.getOrDefault("deny-non-minecraft-request", true), "deny-non-minecraft-request"); - + boolean strictValidation = ResourceConfigUtils.getAsBoolean(arguments.getOrDefault("strict-validation", true), "strict-validation"); Bandwidth limit = null; Map rateLimitingSection = ResourceConfigUtils.getAsMapOrNull(arguments.get("rate-limiting"), "rate-limiting"); @@ -98,7 +98,7 @@ public class SelfHost implements ResourcePackHost { maxBandwidthUsage = ResourceConfigUtils.getAsLong(rateLimitingSection.getOrDefault("max-bandwidth-per-second", 0), "max-bandwidth"); minDownloadSpeed = ResourceConfigUtils.getAsLong(rateLimitingSection.getOrDefault("min-download-speed-per-player", 50_000), "min-download-speed-per-player"); } - selfHostHttpServer.updateProperties(ip, port, url, denyNonMinecraftRequest, protocol, limit, oneTimeToken, maxBandwidthUsage, minDownloadSpeed); + selfHostHttpServer.updateProperties(ip, port, url, denyNonMinecraftRequest, protocol, limit, oneTimeToken, maxBandwidthUsage, minDownloadSpeed, strictValidation); return INSTANCE; } } 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 9a88c719c..da20a14e3 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 @@ -33,6 +33,8 @@ import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Duration; +import java.util.Collections; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -41,7 +43,7 @@ import java.util.concurrent.atomic.AtomicLong; public class SelfHostHttpServer { private static SelfHostHttpServer instance; - private final Cache oneTimePackUrls = Caffeine.newBuilder() + private final Cache oneTimePackUrls = Caffeine.newBuilder() .maximumSize(1024) .scheduler(Scheduler.systemScheduler()) .expireAfterWrite(1, TimeUnit.MINUTES) @@ -67,6 +69,7 @@ public class SelfHostHttpServer { private String url; private boolean denyNonMinecraft = true; private boolean useToken; + private boolean strictValidation = true; private long globalUploadRateLimit = 0; private long minDownloadSpeed = 50_000; @@ -97,13 +100,15 @@ public class SelfHostHttpServer { Bandwidth limitPerIp, boolean token, long globalUploadRateLimit, - long minDownloadSpeed) { + long minDownloadSpeed, + boolean strictValidation) { this.ip = ip; this.url = url; this.denyNonMinecraft = denyNonMinecraft; this.protocol = protocol; this.limitPerIp = limitPerIp; this.useToken = token; + this.strictValidation = strictValidation; if (this.globalUploadRateLimit != globalUploadRateLimit || this.minDownloadSpeed != minDownloadSpeed) { this.globalUploadRateLimit = globalUploadRateLimit; this.minDownloadSpeed = minDownloadSpeed; @@ -214,8 +219,9 @@ public class SelfHostHttpServer { private void handleDownload(ChannelHandlerContext ctx, FullHttpRequest request, QueryStringDecoder queryDecoder) { // 使用一次性token if (useToken) { - String token = queryDecoder.parameters().getOrDefault("token", java.util.Collections.emptyList()).stream().findFirst().orElse(null); - if (!validateToken(token)) { + String token = queryDecoder.parameters().getOrDefault("token", Collections.emptyList()).stream().findFirst().orElse(null); + String clientUUID = strictValidation ? request.headers().get("X-Minecraft-UUID") : null; + if (!validateToken(token, clientUUID)) { sendError(ctx, HttpResponseStatus.FORBIDDEN, "Invalid token"); blockedRequests.incrementAndGet(); return; @@ -225,7 +231,12 @@ public class SelfHostHttpServer { // 不是Minecraft客户端 if (denyNonMinecraft) { String userAgent = request.headers().get(HttpHeaderNames.USER_AGENT); - if (userAgent == null || !userAgent.startsWith("Minecraft Java/")) { + boolean nonMinecraftClient = userAgent == null || !userAgent.startsWith("Minecraft Java/"); + if (strictValidation && !nonMinecraftClient) { + String clientVersion = request.headers().get("X-Minecraft-Version"); + nonMinecraftClient = !Objects.equals(clientVersion, userAgent.substring(15)); + } + if (nonMinecraftClient) { sendError(ctx, HttpResponseStatus.FORBIDDEN, "Invalid client"); blockedRequests.incrementAndGet(); return; @@ -300,10 +311,11 @@ public class SelfHostHttpServer { return rateLimiter.tryConsume(1); } - private boolean validateToken(String token) { + private boolean validateToken(String token, String clientUUID) { if (token == null || token.length() != 36) return false; - Boolean valid = oneTimePackUrls.getIfPresent(token); - if (valid != null) { + String valid = oneTimePackUrls.getIfPresent(token); + boolean isValid = strictValidation ? Objects.equals(valid, clientUUID) : valid != null; + if (isValid) { oneTimePackUrls.invalidate(token); return true; } @@ -348,7 +360,7 @@ public class SelfHostHttpServer { } @Nullable - public ResourcePackDownloadData generateOneTimeUrl() { + public ResourcePackDownloadData generateOneTimeUrl(UUID user) { if (this.resourcePackBytes == null) return null; if (!this.useToken) { @@ -356,7 +368,7 @@ public class SelfHostHttpServer { } String token = UUID.randomUUID().toString(); - oneTimePackUrls.put(token, true); + oneTimePackUrls.put(token, strictValidation ? user.toString().replace("-", "") : ""); return new ResourcePackDownloadData( url() + "download?token=" + URLEncoder.encode(token, StandardCharsets.UTF_8), packUUID,