mirror of
https://github.com/Xiao-MoMi/craft-engine.git
synced 2025-12-19 15:09:15 +00:00
Merge pull request #482 from jhqwqmc/feat/enhanced-self-host-download-verification
增强自托管的验证
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -28,7 +28,7 @@ public class SelfHost implements ResourcePackHost {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<ResourcePackDownloadData>> 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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, Boolean> oneTimePackUrls = Caffeine.newBuilder()
|
||||
private final Cache<String, String> 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,
|
||||
|
||||
Reference in New Issue
Block a user