mirror of
https://github.com/Xiao-MoMi/craft-engine.git
synced 2025-12-19 15:09:15 +00:00
使用netty实现self host
This commit is contained in:
@@ -175,13 +175,6 @@ debug_is_section_injected:
|
||||
- /craftengine debug is-section-injected
|
||||
- /ce debug is-section-injected
|
||||
|
||||
debug_host_status:
|
||||
enable: true
|
||||
permission: ce.command.debug.host_status
|
||||
usage:
|
||||
- /craftengine debug host-status
|
||||
- /ce debug host-status
|
||||
|
||||
debug_test:
|
||||
enable: true
|
||||
permission: ce.command.debug.test
|
||||
|
||||
@@ -39,7 +39,6 @@ public class BukkitCommandManager extends AbstractCommandManager<CommandSender>
|
||||
new SearchUsageAdminCommand(this, plugin),
|
||||
new TestCommand(this, plugin),
|
||||
new DebugGetBlockStateRegistryIdCommand(this, plugin),
|
||||
new DebugHostStatusCommand(this, plugin),
|
||||
new DebugGetBlockInternalIdCommand(this, plugin),
|
||||
new DebugAppearanceStateUsageCommand(this, plugin),
|
||||
new DebugRealStateUsageCommand(this, plugin),
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package net.momirealms.craftengine.bukkit.plugin.command.feature;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.momirealms.craftengine.bukkit.nms.FastNMS;
|
||||
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
|
||||
import net.momirealms.craftengine.bukkit.plugin.injector.BukkitInjector;
|
||||
import net.momirealms.craftengine.core.pack.host.impl.SelfHostHttpServer;
|
||||
import net.momirealms.craftengine.core.plugin.CraftEngine;
|
||||
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
|
||||
import net.momirealms.craftengine.core.plugin.command.sender.Sender;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.incendo.cloud.Command;
|
||||
|
||||
public class DebugHostStatusCommand extends BukkitCommandFeature<CommandSender> {
|
||||
|
||||
public DebugHostStatusCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
|
||||
super(commandManager, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
|
||||
return builder
|
||||
.handler(context -> {
|
||||
Sender sender = plugin().senderFactory().wrap(context.sender());
|
||||
sender.sendMessage(Component.text("Self Host status: " + (SelfHostHttpServer.instance().isAlive() ? "on" : "off")));
|
||||
byte[] pack = SelfHostHttpServer.instance().resourcePackBytes();
|
||||
sender.sendMessage(Component.text("Resource Pack Bytes: " + (pack == null ? "null" : pack.length)));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFeatureID() {
|
||||
return "debug_host_status";
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ dependencies {
|
||||
compileOnly("org.apache.logging.log4j:log4j-core:${rootProject.properties["log4j_version"]}")
|
||||
// Netty
|
||||
compileOnly("io.netty:netty-all:${rootProject.properties["netty_version"]}")
|
||||
compileOnly("io.netty:netty-codec-http:${rootProject.properties["netty_version"]}")
|
||||
// Cache
|
||||
compileOnly("com.github.ben-manes.caffeine:caffeine:${rootProject.properties["caffeine_version"]}")
|
||||
// Compression
|
||||
|
||||
@@ -15,7 +15,6 @@ public class S3HostFactory implements ResourcePackHostFactory {
|
||||
public ResourcePackHost create(Map<String, Object> arguments) {
|
||||
CraftEngine.instance().dependencyManager().loadDependencies(
|
||||
List.of(
|
||||
Dependencies.NETTY_HTTP,
|
||||
Dependencies.NETTY_HTTP2,
|
||||
Dependencies.REACTIVE_STREAMS,
|
||||
Dependencies.AMAZON_AWSSDK_S3,
|
||||
|
||||
@@ -3,15 +3,19 @@ package net.momirealms.craftengine.core.pack.host.impl;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.Scheduler;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.http.*;
|
||||
import io.netty.util.CharsetUtil;
|
||||
import net.momirealms.craftengine.core.pack.host.ResourcePackDownloadData;
|
||||
import net.momirealms.craftengine.core.plugin.CraftEngine;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -19,30 +23,26 @@ 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.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class SelfHostHttpServer {
|
||||
private static SelfHostHttpServer instance;
|
||||
|
||||
// Caffeine缓存和统计计数器
|
||||
private final Cache<String, Boolean> oneTimePackUrls = Caffeine.newBuilder()
|
||||
.maximumSize(256)
|
||||
.scheduler(Scheduler.systemScheduler())
|
||||
.expireAfterWrite(1, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
private final Cache<String, IpAccessRecord> ipAccessCache = Caffeine.newBuilder()
|
||||
.maximumSize(256)
|
||||
.scheduler(Scheduler.systemScheduler())
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
private ExecutorService threadPool;
|
||||
private HttpServer server;
|
||||
|
||||
private final AtomicLong totalRequests = new AtomicLong();
|
||||
private final AtomicLong blockedRequests = new AtomicLong();
|
||||
|
||||
@@ -59,47 +59,9 @@ public class SelfHostHttpServer {
|
||||
private String packHash;
|
||||
private UUID packUUID;
|
||||
|
||||
public void updateProperties(String ip,
|
||||
int port,
|
||||
String url,
|
||||
boolean denyNonMinecraft,
|
||||
String protocol,
|
||||
int maxRequests,
|
||||
int resetInternal,
|
||||
boolean token) {
|
||||
this.ip = ip;
|
||||
this.url = url;
|
||||
this.denyNonMinecraft = denyNonMinecraft;
|
||||
this.protocol = protocol;
|
||||
this.rateLimit = maxRequests;
|
||||
this.rateLimitInterval = resetInternal;
|
||||
this.useToken = token;
|
||||
if (port <= 0 || port > 65535) {
|
||||
throw new IllegalArgumentException("Invalid port number: " + port);
|
||||
}
|
||||
if (port == this.port && this.server != null) return;
|
||||
if (this.server != null) disable();
|
||||
this.port = port;
|
||||
try {
|
||||
this.threadPool = Executors.newFixedThreadPool(1);
|
||||
this.server = HttpServer.create(new InetSocketAddress("::", port), 0);
|
||||
this.server.createContext("/download", new ResourcePackHandler());
|
||||
// this.server.createContext("/metrics", this::handleMetrics);
|
||||
this.server.setExecutor(this.threadPool);
|
||||
this.server.start();
|
||||
CraftEngine.instance().logger().info("HTTP server started on port: " + port);
|
||||
} catch (IOException e) {
|
||||
CraftEngine.instance().logger().warn("Failed to start HTTP server", e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
return this.server != null;
|
||||
}
|
||||
|
||||
public byte[] resourcePackBytes() {
|
||||
return resourcePackBytes;
|
||||
}
|
||||
private EventLoopGroup bossGroup;
|
||||
private EventLoopGroup workerGroup;
|
||||
private Channel serverChannel;
|
||||
|
||||
public static SelfHostHttpServer instance() {
|
||||
if (instance == null) {
|
||||
@@ -108,21 +70,31 @@ public class SelfHostHttpServer {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ResourcePackDownloadData generateOneTimeUrl() {
|
||||
if (this.resourcePackBytes == null) {
|
||||
return null;
|
||||
public void updateProperties(String ip,
|
||||
int port,
|
||||
String url,
|
||||
boolean denyNonMinecraft,
|
||||
String protocol,
|
||||
int maxRequests,
|
||||
int resetInterval,
|
||||
boolean token) {
|
||||
this.ip = ip;
|
||||
this.url = url;
|
||||
this.denyNonMinecraft = denyNonMinecraft;
|
||||
this.protocol = protocol;
|
||||
this.rateLimit = maxRequests;
|
||||
this.rateLimitInterval = resetInterval;
|
||||
this.useToken = token;
|
||||
|
||||
if (port <= 0 || port > 65535) {
|
||||
throw new IllegalArgumentException("Invalid port: " + port);
|
||||
}
|
||||
if (!this.useToken) {
|
||||
return new ResourcePackDownloadData(url() + "download", this.packUUID, this.packHash);
|
||||
}
|
||||
String token = UUID.randomUUID().toString();
|
||||
this.oneTimePackUrls.put(token, true);
|
||||
return new ResourcePackDownloadData(
|
||||
url() + "download?token=" + URLEncoder.encode(token, StandardCharsets.UTF_8),
|
||||
this.packUUID,
|
||||
this.packHash
|
||||
);
|
||||
|
||||
if (this.port == port && serverChannel != null) return;
|
||||
disable();
|
||||
|
||||
this.port = port;
|
||||
initializeServer();
|
||||
}
|
||||
|
||||
public String url() {
|
||||
@@ -132,6 +104,189 @@ public class SelfHostHttpServer {
|
||||
return this.protocol + "://" + this.ip + ":" + this.port + "/";
|
||||
}
|
||||
|
||||
private void initializeServer() {
|
||||
bossGroup = new NioEventLoopGroup(1);
|
||||
workerGroup = new NioEventLoopGroup();
|
||||
|
||||
ServerBootstrap b = new ServerBootstrap();
|
||||
b.group(bossGroup, workerGroup)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) {
|
||||
ChannelPipeline pipeline = ch.pipeline();
|
||||
pipeline.addLast(new HttpServerCodec());
|
||||
pipeline.addLast(new HttpObjectAggregator(1048576));
|
||||
pipeline.addLast(new RequestHandler());
|
||||
}
|
||||
});
|
||||
try {
|
||||
serverChannel = b.bind(port).sync().channel();
|
||||
CraftEngine.instance().logger().info("Netty HTTP server started on port: " + port);
|
||||
} catch (InterruptedException e) {
|
||||
CraftEngine.instance().logger().warn("Failed to start Netty server", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
private class RequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
|
||||
totalRequests.incrementAndGet();
|
||||
|
||||
try {
|
||||
String clientIp = ((InetSocketAddress) ctx.channel().remoteAddress())
|
||||
.getAddress().getHostAddress();
|
||||
|
||||
if (checkRateLimit(clientIp)) {
|
||||
sendError(ctx, HttpResponseStatus.TOO_MANY_REQUESTS, "Rate limit exceeded");
|
||||
blockedRequests.incrementAndGet();
|
||||
return;
|
||||
}
|
||||
|
||||
QueryStringDecoder queryDecoder = new QueryStringDecoder(request.uri());
|
||||
String path = queryDecoder.path();
|
||||
|
||||
if ("/download".equals(path)) {
|
||||
handleDownload(ctx, request, queryDecoder);
|
||||
} else if ("/metrics".equals(path)) {
|
||||
handleMetrics(ctx);
|
||||
} else {
|
||||
sendError(ctx, HttpResponseStatus.NOT_FOUND, "Not Found");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
CraftEngine.instance().logger().warn("Request handling failed", e);
|
||||
sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR, "Internal Error");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDownload(ChannelHandlerContext ctx, FullHttpRequest request, QueryStringDecoder queryDecoder) {
|
||||
if (useToken) {
|
||||
String token = queryDecoder.parameters().getOrDefault("token", java.util.Collections.emptyList()).stream().findFirst().orElse(null);
|
||||
if (!validateToken(token)) {
|
||||
sendError(ctx, HttpResponseStatus.FORBIDDEN, "Invalid token");
|
||||
blockedRequests.incrementAndGet();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (denyNonMinecraft) {
|
||||
String userAgent = request.headers().get(HttpHeaderNames.USER_AGENT);
|
||||
if (userAgent == null || !userAgent.startsWith("Minecraft Java/")) {
|
||||
sendError(ctx, HttpResponseStatus.FORBIDDEN, "Invalid client");
|
||||
blockedRequests.incrementAndGet();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (resourcePackBytes == null) {
|
||||
sendError(ctx, HttpResponseStatus.NOT_FOUND, "Resource pack missing");
|
||||
blockedRequests.incrementAndGet();
|
||||
return;
|
||||
}
|
||||
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(
|
||||
HttpVersion.HTTP_1_1,
|
||||
HttpResponseStatus.OK,
|
||||
Unpooled.wrappedBuffer(resourcePackBytes)
|
||||
);
|
||||
response.headers()
|
||||
.set(HttpHeaderNames.CONTENT_TYPE, "application/zip")
|
||||
.set(HttpHeaderNames.CONTENT_LENGTH, resourcePackBytes.length);
|
||||
|
||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
private void handleMetrics(ChannelHandlerContext ctx) {
|
||||
String metrics = "# TYPE total_requests counter\n"
|
||||
+ "total_requests " + totalRequests.get() + "\n"
|
||||
+ "# TYPE blocked_requests counter\n"
|
||||
+ "blocked_requests " + blockedRequests.get();
|
||||
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(
|
||||
HttpVersion.HTTP_1_1,
|
||||
HttpResponseStatus.OK,
|
||||
Unpooled.copiedBuffer(metrics, CharsetUtil.UTF_8)
|
||||
);
|
||||
response.headers()
|
||||
.set(HttpHeaderNames.CONTENT_TYPE, "text/plain")
|
||||
.set(HttpHeaderNames.CONTENT_LENGTH, metrics.length());
|
||||
|
||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (now - record.lastAccessTime > rateLimitInterval) {
|
||||
record.lastAccessTime = now;
|
||||
record.accessCount = 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
return ++record.accessCount > rateLimit;
|
||||
}
|
||||
|
||||
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 void sendError(ChannelHandlerContext ctx, HttpResponseStatus status, String message) {
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(
|
||||
HttpVersion.HTTP_1_1,
|
||||
status,
|
||||
Unpooled.copiedBuffer(message, CharsetUtil.UTF_8)
|
||||
);
|
||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
CraftEngine.instance().logger().warn("Channel error", cause);
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ResourcePackDownloadData generateOneTimeUrl() {
|
||||
if (this.resourcePackBytes == null) return null;
|
||||
|
||||
if (!this.useToken) {
|
||||
return new ResourcePackDownloadData(url() + "download", this.packUUID, this.packHash);
|
||||
}
|
||||
|
||||
String token = UUID.randomUUID().toString();
|
||||
oneTimePackUrls.put(token, true);
|
||||
return new ResourcePackDownloadData(
|
||||
url() + "download?token=" + URLEncoder.encode(token, StandardCharsets.UTF_8),
|
||||
packUUID,
|
||||
packHash
|
||||
);
|
||||
}
|
||||
|
||||
public void disable() {
|
||||
if (serverChannel != null) {
|
||||
serverChannel.close().awaitUninterruptibly();
|
||||
bossGroup.shutdownGracefully();
|
||||
workerGroup.shutdownGracefully();
|
||||
serverChannel = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void readResourcePack(Path path) {
|
||||
try {
|
||||
if (Files.exists(path)) {
|
||||
@@ -162,151 +317,8 @@ public class SelfHostHttpServer {
|
||||
}
|
||||
}
|
||||
|
||||
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 (this.server != null) {
|
||||
this.server.stop(0);
|
||||
this.server = null;
|
||||
if (this.threadPool != null) {
|
||||
this.threadPool.shutdownNow();
|
||||
this.threadPool = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ResourcePackHandler implements HttpHandler {
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
totalRequests.incrementAndGet();
|
||||
|
||||
String clientIp = getClientIp(exchange);
|
||||
if (checkRateLimit(clientIp)) {
|
||||
handleBlockedRequest(exchange, 429, "Rate limit exceeded");
|
||||
return;
|
||||
}
|
||||
if (useToken) {
|
||||
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);
|
||||
} catch (Exception e) {
|
||||
handleBlockedRequest(exchange, 500, "Internal error");
|
||||
CraftEngine.instance().logger().warn("Request handling failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
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<String, String> 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 (!denyNonMinecraft) 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) {
|
||||
if (!e.getMessage().contains("abort") && !e.getMessage().contains("reset")) {
|
||||
CraftEngine.instance().logger().warn("Failed to send resource pack", e);
|
||||
throw e;
|
||||
}
|
||||
CraftEngine.instance().debug(() -> "Client aborted resource pack download: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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<String, String> parseQuery(String query) {
|
||||
Map<String, String> 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;
|
||||
long lastAccessTime;
|
||||
int accessCount;
|
||||
|
||||
IpAccessRecord(long lastAccessTime, int accessCount) {
|
||||
|
||||
@@ -315,6 +315,7 @@ public abstract class CraftEngine implements Plugin {
|
||||
Dependencies.AHO_CORASICK,
|
||||
Dependencies.LZ4,
|
||||
Dependencies.EVALEX,
|
||||
Dependencies.NETTY_HTTP,
|
||||
Dependencies.JIMFS,
|
||||
Dependencies.COMMONS_IMAGING
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ commons_imaging_version=1.0.0-alpha6
|
||||
sparrow_nbt_version=0.7.3
|
||||
sparrow_util_version=0.47
|
||||
fastutil_version=8.5.15
|
||||
netty_version=4.1.119.Final
|
||||
netty_version=4.1.121.Final
|
||||
joml_version=1.10.8
|
||||
datafixerupper_version=6.0.8
|
||||
mojang_brigadier_version=1.0.18
|
||||
|
||||
Reference in New Issue
Block a user