9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-19 15:09:15 +00:00

增强自托管的验证

This commit is contained in:
jhqwqmc
2025-11-29 04:08:48 +08:00
parent 19d1cb2a21
commit e54e622580
3 changed files with 27 additions and 13 deletions

View File

@@ -197,6 +197,8 @@ resource-pack:
deny-non-minecraft-request: true deny-non-minecraft-request: true
# Generates a single-use, time-limited download link for each player. # Generates a single-use, time-limited download link for each player.
one-time-token: true one-time-token: true
# Enhances validation for deny-non-minecraft-request and one-time-token.
strict-validation: true
rate-limiting: rate-limiting:
# Maximum bandwidth per second to prevent server instability for other players during resource pack downloads # Maximum bandwidth per second to prevent server instability for other players during resource pack downloads
max-bandwidth-per-second: 5_000_000 # 5MB/s max-bandwidth-per-second: 5_000_000 # 5MB/s

View File

@@ -28,7 +28,7 @@ public class SelfHost implements ResourcePackHost {
@Override @Override
public CompletableFuture<List<ResourcePackDownloadData>> requestResourcePackDownloadLink(UUID player) { 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()); if (data == null) return CompletableFuture.completedFuture(List.of());
return CompletableFuture.completedFuture(List.of(data)); 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"); boolean oneTimeToken = ResourceConfigUtils.getAsBoolean(arguments.getOrDefault("one-time-token", true), "one-time-token");
String protocol = arguments.getOrDefault("protocol", "http").toString(); String protocol = arguments.getOrDefault("protocol", "http").toString();
boolean denyNonMinecraftRequest = ResourceConfigUtils.getAsBoolean(arguments.getOrDefault("deny-non-minecraft-request", true), "deny-non-minecraft-request"); 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; Bandwidth limit = null;
Map<String, Object> rateLimitingSection = ResourceConfigUtils.getAsMapOrNull(arguments.get("rate-limiting"), "rate-limiting"); 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"); 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"); 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; return INSTANCE;
} }
} }

View File

@@ -33,6 +33,8 @@ import java.nio.file.Path;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Duration; import java.time.Duration;
import java.util.Collections;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@@ -41,7 +43,7 @@ import java.util.concurrent.atomic.AtomicLong;
public class SelfHostHttpServer { public class SelfHostHttpServer {
private static SelfHostHttpServer instance; private static SelfHostHttpServer instance;
private final Cache<String, Boolean> oneTimePackUrls = Caffeine.newBuilder() private final Cache<String, String> oneTimePackUrls = Caffeine.newBuilder()
.maximumSize(1024) .maximumSize(1024)
.scheduler(Scheduler.systemScheduler()) .scheduler(Scheduler.systemScheduler())
.expireAfterWrite(1, TimeUnit.MINUTES) .expireAfterWrite(1, TimeUnit.MINUTES)
@@ -67,6 +69,7 @@ public class SelfHostHttpServer {
private String url; private String url;
private boolean denyNonMinecraft = true; private boolean denyNonMinecraft = true;
private boolean useToken; private boolean useToken;
private boolean strictValidation = true;
private long globalUploadRateLimit = 0; private long globalUploadRateLimit = 0;
private long minDownloadSpeed = 50_000; private long minDownloadSpeed = 50_000;
@@ -97,13 +100,15 @@ public class SelfHostHttpServer {
Bandwidth limitPerIp, Bandwidth limitPerIp,
boolean token, boolean token,
long globalUploadRateLimit, long globalUploadRateLimit,
long minDownloadSpeed) { long minDownloadSpeed,
boolean strictValidation) {
this.ip = ip; this.ip = ip;
this.url = url; this.url = url;
this.denyNonMinecraft = denyNonMinecraft; this.denyNonMinecraft = denyNonMinecraft;
this.protocol = protocol; this.protocol = protocol;
this.limitPerIp = limitPerIp; this.limitPerIp = limitPerIp;
this.useToken = token; this.useToken = token;
this.strictValidation = strictValidation;
if (this.globalUploadRateLimit != globalUploadRateLimit || this.minDownloadSpeed != minDownloadSpeed) { if (this.globalUploadRateLimit != globalUploadRateLimit || this.minDownloadSpeed != minDownloadSpeed) {
this.globalUploadRateLimit = globalUploadRateLimit; this.globalUploadRateLimit = globalUploadRateLimit;
this.minDownloadSpeed = minDownloadSpeed; this.minDownloadSpeed = minDownloadSpeed;
@@ -214,8 +219,9 @@ public class SelfHostHttpServer {
private void handleDownload(ChannelHandlerContext ctx, FullHttpRequest request, QueryStringDecoder queryDecoder) { private void handleDownload(ChannelHandlerContext ctx, FullHttpRequest request, QueryStringDecoder queryDecoder) {
// 使用一次性token // 使用一次性token
if (useToken) { if (useToken) {
String token = queryDecoder.parameters().getOrDefault("token", java.util.Collections.emptyList()).stream().findFirst().orElse(null); String token = queryDecoder.parameters().getOrDefault("token", Collections.emptyList()).stream().findFirst().orElse(null);
if (!validateToken(token)) { String clientUUID = strictValidation ? request.headers().get("X-Minecraft-UUID") : null;
if (!validateToken(token, clientUUID)) {
sendError(ctx, HttpResponseStatus.FORBIDDEN, "Invalid token"); sendError(ctx, HttpResponseStatus.FORBIDDEN, "Invalid token");
blockedRequests.incrementAndGet(); blockedRequests.incrementAndGet();
return; return;
@@ -225,7 +231,12 @@ public class SelfHostHttpServer {
// 不是Minecraft客户端 // 不是Minecraft客户端
if (denyNonMinecraft) { if (denyNonMinecraft) {
String userAgent = request.headers().get(HttpHeaderNames.USER_AGENT); 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"); sendError(ctx, HttpResponseStatus.FORBIDDEN, "Invalid client");
blockedRequests.incrementAndGet(); blockedRequests.incrementAndGet();
return; return;
@@ -300,10 +311,11 @@ public class SelfHostHttpServer {
return rateLimiter.tryConsume(1); return rateLimiter.tryConsume(1);
} }
private boolean validateToken(String token) { private boolean validateToken(String token, String clientUUID) {
if (token == null || token.length() != 36) return false; if (token == null || token.length() != 36) return false;
Boolean valid = oneTimePackUrls.getIfPresent(token); String valid = oneTimePackUrls.getIfPresent(token);
if (valid != null) { boolean isValid = strictValidation ? Objects.equals(valid, clientUUID) : valid != null;
if (isValid) {
oneTimePackUrls.invalidate(token); oneTimePackUrls.invalidate(token);
return true; return true;
} }
@@ -348,7 +360,7 @@ public class SelfHostHttpServer {
} }
@Nullable @Nullable
public ResourcePackDownloadData generateOneTimeUrl() { public ResourcePackDownloadData generateOneTimeUrl(UUID user) {
if (this.resourcePackBytes == null) return null; if (this.resourcePackBytes == null) return null;
if (!this.useToken) { if (!this.useToken) {
@@ -356,7 +368,7 @@ public class SelfHostHttpServer {
} }
String token = UUID.randomUUID().toString(); String token = UUID.randomUUID().toString();
oneTimePackUrls.put(token, true); oneTimePackUrls.put(token, strictValidation ? user.toString().replace("-", "") : "");
return new ResourcePackDownloadData( return new ResourcePackDownloadData(
url() + "download?token=" + URLEncoder.encode(token, StandardCharsets.UTF_8), url() + "download?token=" + URLEncoder.encode(token, StandardCharsets.UTF_8),
packUUID, packUUID,