2025-11-24 22:52:51 +03:00

286 lines
14 KiB
Java

/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* com.google.common.annotations.VisibleForTesting
* com.google.gson.JsonArray
* com.google.gson.JsonElement
* com.google.gson.JsonNull
* com.google.gson.JsonObject
* com.google.gson.JsonParseException
* com.mojang.logging.LogUtils
* io.netty.channel.Channel
* io.netty.channel.ChannelHandlerContext
* io.netty.channel.SimpleChannelInboundHandler
* io.netty.handler.timeout.ReadTimeoutException
* it.unimi.dsi.fastutil.ints.Int2ObjectMap
* it.unimi.dsi.fastutil.ints.Int2ObjectMaps
* it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
* org.jetbrains.annotations.Contract
* org.jspecify.annotations.Nullable
* org.slf4j.Logger
*/
package net.minecraft.server.jsonrpc;
import com.google.common.annotations.VisibleForTesting;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.mojang.logging.LogUtils;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.ReadTimeoutException;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.jsonrpc.IncomingRpcMethod;
import net.minecraft.server.jsonrpc.JsonRPCErrors;
import net.minecraft.server.jsonrpc.JsonRPCUtils;
import net.minecraft.server.jsonrpc.JsonRpcLogger;
import net.minecraft.server.jsonrpc.ManagementServer;
import net.minecraft.server.jsonrpc.OutgoingRpcMethod;
import net.minecraft.server.jsonrpc.PendingRpcRequest;
import net.minecraft.server.jsonrpc.internalapi.MinecraftApi;
import net.minecraft.server.jsonrpc.methods.ClientInfo;
import net.minecraft.server.jsonrpc.methods.EncodeJsonRpcException;
import net.minecraft.server.jsonrpc.methods.InvalidParameterJsonRpcException;
import net.minecraft.server.jsonrpc.methods.InvalidRequestJsonRpcException;
import net.minecraft.server.jsonrpc.methods.MethodNotFoundJsonRpcException;
import net.minecraft.server.jsonrpc.methods.RemoteRpcErrorException;
import net.minecraft.util.GsonHelper;
import net.minecraft.util.Util;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
public class Connection
extends SimpleChannelInboundHandler<JsonElement> {
private static final Logger LOGGER = LogUtils.getLogger();
private static final AtomicInteger CONNECTION_ID_COUNTER = new AtomicInteger(0);
private final JsonRpcLogger jsonRpcLogger;
private final ClientInfo clientInfo;
private final ManagementServer managementServer;
private final Channel channel;
private final MinecraftApi minecraftApi;
private final AtomicInteger transactionId = new AtomicInteger();
private final Int2ObjectMap<PendingRpcRequest<?>> pendingRequests = Int2ObjectMaps.synchronize((Int2ObjectMap)new Int2ObjectOpenHashMap());
public Connection(Channel channel, ManagementServer managementServer, MinecraftApi minecraftApi, JsonRpcLogger jsonrpcLogger) {
this.clientInfo = ClientInfo.of(CONNECTION_ID_COUNTER.incrementAndGet());
this.managementServer = managementServer;
this.minecraftApi = minecraftApi;
this.channel = channel;
this.jsonRpcLogger = jsonrpcLogger;
}
public void tick() {
long time = Util.getMillis();
this.pendingRequests.int2ObjectEntrySet().removeIf(entry -> {
boolean timedOut = ((PendingRpcRequest)entry.getValue()).timedOut(time);
if (timedOut) {
((PendingRpcRequest)entry.getValue()).resultFuture().completeExceptionally((Throwable)new ReadTimeoutException("RPC method " + String.valueOf(((PendingRpcRequest)entry.getValue()).method().key().identifier()) + " timed out waiting for response"));
}
return timedOut;
});
}
public void channelActive(ChannelHandlerContext ctx) throws Exception {
this.jsonRpcLogger.log(this.clientInfo, "Management connection opened for {}", this.channel.remoteAddress());
super.channelActive(ctx);
this.managementServer.onConnected(this);
}
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
this.jsonRpcLogger.log(this.clientInfo, "Management connection closed for {}", this.channel.remoteAddress());
super.channelInactive(ctx);
this.managementServer.onDisconnected(this);
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (cause.getCause() instanceof JsonParseException) {
this.channel.writeAndFlush((Object)JsonRPCErrors.PARSE_ERROR.createWithUnknownId(cause.getMessage()));
return;
}
super.exceptionCaught(ctx, cause);
this.channel.close().awaitUninterruptibly();
}
protected void channelRead0(ChannelHandlerContext channelHandlerContext, JsonElement jsonElement) {
if (jsonElement.isJsonObject()) {
JsonObject response = this.handleJsonObject(jsonElement.getAsJsonObject());
if (response != null) {
this.channel.writeAndFlush((Object)response);
}
} else if (jsonElement.isJsonArray()) {
this.channel.writeAndFlush((Object)this.handleBatchRequest(jsonElement.getAsJsonArray().asList()));
} else {
this.channel.writeAndFlush((Object)JsonRPCErrors.INVALID_REQUEST.createWithUnknownId(null));
}
}
private JsonArray handleBatchRequest(List<JsonElement> batchRequests) {
JsonArray batchResponses = new JsonArray();
batchRequests.stream().map(batchEntry -> this.handleJsonObject(batchEntry.getAsJsonObject())).filter(Objects::nonNull).forEach(arg_0 -> ((JsonArray)batchResponses).add(arg_0));
return batchResponses;
}
public void sendNotification(Holder.Reference<? extends OutgoingRpcMethod<Void, ?>> method) {
this.sendRequest(method, null, false);
}
public <Params> void sendNotification(Holder.Reference<? extends OutgoingRpcMethod<Params, ?>> method, Params params) {
this.sendRequest(method, params, false);
}
public <Result> CompletableFuture<Result> sendRequest(Holder.Reference<? extends OutgoingRpcMethod<Void, Result>> method) {
return this.sendRequest(method, null, true);
}
public <Params, Result> CompletableFuture<Result> sendRequest(Holder.Reference<? extends OutgoingRpcMethod<Params, Result>> method, Params params) {
return this.sendRequest(method, params, true);
}
@Contract(value="_,_,false->null;_,_,true->!null")
private <Params, Result> @Nullable CompletableFuture<Result> sendRequest(Holder.Reference<? extends OutgoingRpcMethod<Params, ? extends Result>> method, @Nullable Params params, boolean expectReply) {
List<JsonElement> jsonParams;
List<JsonElement> list = jsonParams = params != null ? List.of(Objects.requireNonNull(method.value().encodeParams(params))) : List.of();
if (expectReply) {
CompletableFuture future = new CompletableFuture();
int id = this.transactionId.incrementAndGet();
long time = Util.timeSource.get(TimeUnit.MILLISECONDS);
this.pendingRequests.put(id, new PendingRpcRequest(method, future, time + 5000L));
this.channel.writeAndFlush((Object)JsonRPCUtils.createRequest(id, method.key().identifier(), jsonParams));
return future;
}
this.channel.writeAndFlush((Object)JsonRPCUtils.createRequest(null, method.key().identifier(), jsonParams));
return null;
}
@VisibleForTesting
@Nullable JsonObject handleJsonObject(JsonObject jsonObject) {
try {
JsonElement id = JsonRPCUtils.getRequestId(jsonObject);
String method = JsonRPCUtils.getMethodName(jsonObject);
JsonElement result = JsonRPCUtils.getResult(jsonObject);
JsonElement params = JsonRPCUtils.getParams(jsonObject);
JsonObject error = JsonRPCUtils.getError(jsonObject);
if (method != null && result == null && error == null) {
if (id != null && !Connection.isValidRequestId(id)) {
return JsonRPCErrors.INVALID_REQUEST.createWithUnknownId("Invalid request id - only String, Number and NULL supported");
}
return this.handleIncomingRequest(id, method, params);
}
if (method == null && result != null && error == null && id != null) {
if (Connection.isValidResponseId(id)) {
this.handleRequestResponse(id.getAsInt(), result);
} else {
LOGGER.warn("Received respose {} with id {} we did not request", (Object)result, (Object)id);
}
return null;
}
if (method == null && result == null && error != null) {
return this.handleError(id, error);
}
return JsonRPCErrors.INVALID_REQUEST.createWithoutData((JsonElement)Objects.requireNonNullElse(id, JsonNull.INSTANCE));
}
catch (Exception e) {
LOGGER.error("Error while handling rpc request", (Throwable)e);
return JsonRPCErrors.INTERNAL_ERROR.createWithUnknownId("Unknown error handling request - check server logs for stack trace");
}
}
private static boolean isValidRequestId(JsonElement id) {
return id.isJsonNull() || GsonHelper.isNumberValue(id) || GsonHelper.isStringValue(id);
}
private static boolean isValidResponseId(JsonElement id) {
return GsonHelper.isNumberValue(id);
}
private @Nullable JsonObject handleIncomingRequest(@Nullable JsonElement id, String method, @Nullable JsonElement params) {
boolean sendResponse = id != null;
try {
JsonElement result = this.dispatchIncomingRequest(method, params);
if (result == null || !sendResponse) {
return null;
}
return JsonRPCUtils.createSuccessResult(id, result);
}
catch (InvalidParameterJsonRpcException e) {
LOGGER.debug("Invalid parameter invocation {}: {}, {}", new Object[]{method, params, e.getMessage()});
return sendResponse ? JsonRPCErrors.INVALID_PARAMS.create(id, e.getMessage()) : null;
}
catch (EncodeJsonRpcException e) {
LOGGER.error("Failed to encode json rpc response {}: {}", (Object)method, (Object)e.getMessage());
return sendResponse ? JsonRPCErrors.INTERNAL_ERROR.create(id, e.getMessage()) : null;
}
catch (InvalidRequestJsonRpcException e) {
return sendResponse ? JsonRPCErrors.INVALID_REQUEST.create(id, e.getMessage()) : null;
}
catch (MethodNotFoundJsonRpcException e) {
return sendResponse ? JsonRPCErrors.METHOD_NOT_FOUND.create(id, e.getMessage()) : null;
}
catch (Exception e) {
LOGGER.error("Error while dispatching rpc method {}", (Object)method, (Object)e);
return sendResponse ? JsonRPCErrors.INTERNAL_ERROR.createWithoutData(id) : null;
}
}
public @Nullable JsonElement dispatchIncomingRequest(String method, @Nullable JsonElement params) {
Identifier identifier = Identifier.tryParse(method);
if (identifier == null) {
throw new InvalidRequestJsonRpcException("Failed to parse method value: " + method);
}
Optional<IncomingRpcMethod<?, ?>> incomingRpcMethod = BuiltInRegistries.INCOMING_RPC_METHOD.getOptional(identifier);
if (incomingRpcMethod.isEmpty()) {
throw new MethodNotFoundJsonRpcException("Method not found: " + method);
}
if (incomingRpcMethod.get().attributes().runOnMainThread()) {
try {
return this.minecraftApi.submit(() -> ((IncomingRpcMethod)incomingRpcMethod.get()).apply(this.minecraftApi, params, this.clientInfo)).join();
}
catch (CompletionException e) {
Throwable throwable = e.getCause();
if (throwable instanceof RuntimeException) {
RuntimeException re = (RuntimeException)throwable;
throw re;
}
throw e;
}
}
return incomingRpcMethod.get().apply(this.minecraftApi, params, this.clientInfo);
}
private void handleRequestResponse(int id, JsonElement result) {
PendingRpcRequest request = (PendingRpcRequest)this.pendingRequests.remove(id);
if (request == null) {
LOGGER.warn("Received unknown response (id: {}): {}", (Object)id, (Object)result);
} else {
request.accept(result);
}
}
private @Nullable JsonObject handleError(@Nullable JsonElement id, JsonObject error) {
PendingRpcRequest request;
if (id != null && Connection.isValidResponseId(id) && (request = (PendingRpcRequest)this.pendingRequests.remove(id.getAsInt())) != null) {
request.resultFuture().completeExceptionally(new RemoteRpcErrorException(id, error));
}
LOGGER.error("Received error (id: {}): {}", (Object)id, (Object)error);
return null;
}
}