2022-03-08 04:09:04 +01:00
|
|
|
package com.comphenix.protocol.injector.netty.channel;
|
|
|
|
|
2022-08-10 22:50:33 +02:00
|
|
|
import java.lang.reflect.Field;
|
|
|
|
import java.lang.reflect.Modifier;
|
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.HashSet;
|
|
|
|
import java.util.Map;
|
|
|
|
import java.util.Map.Entry;
|
|
|
|
import java.util.NoSuchElementException;
|
|
|
|
import java.util.Set;
|
|
|
|
import java.util.WeakHashMap;
|
|
|
|
import java.util.concurrent.Callable;
|
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
2022-10-16 22:31:42 +02:00
|
|
|
import java.util.concurrent.ThreadLocalRandom;
|
2022-08-10 22:50:33 +02:00
|
|
|
|
2022-03-08 04:09:04 +01:00
|
|
|
import com.comphenix.protocol.PacketType;
|
|
|
|
import com.comphenix.protocol.PacketType.Protocol;
|
2023-07-04 03:45:47 +02:00
|
|
|
import com.comphenix.protocol.ProtocolLibrary;
|
2022-03-08 04:09:04 +01:00
|
|
|
import com.comphenix.protocol.error.ErrorReporter;
|
|
|
|
import com.comphenix.protocol.error.Report;
|
|
|
|
import com.comphenix.protocol.error.ReportType;
|
|
|
|
import com.comphenix.protocol.events.NetworkMarker;
|
|
|
|
import com.comphenix.protocol.events.PacketEvent;
|
|
|
|
import com.comphenix.protocol.injector.NetworkProcessor;
|
|
|
|
import com.comphenix.protocol.injector.netty.ChannelListener;
|
|
|
|
import com.comphenix.protocol.injector.netty.Injector;
|
|
|
|
import com.comphenix.protocol.reflect.FuzzyReflection;
|
|
|
|
import com.comphenix.protocol.reflect.accessors.Accessors;
|
|
|
|
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
|
|
|
|
import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract;
|
Packet filtering for bundled packets in 1.19.4 (#2258)
Since Minecraft 1.19.4, the protocol supports bundling consecutive packets to ensure the client processes them in one tick. However, Packet Events are not called for the individual packets in such a bundle in the current dev build of ProtocolLib. For example, no packet events are currently sent for the ENTITY_METADATA packet when an entity is first spawned as the packet is bundled with the ENTITY_SPAWN packet. However, if the entity metadata is changed later on, the event will be called.
This PR proposes to fix this by unpacking the bundled packets and invoking the packet filtering for each packet.
I also want to briefly explain how the bundling works. A bundle starts with a PACKET_DELIMITER (0x00, net.minecraft.network.protocol.BundleDelimiterPacket) packet followed by all packets that should be bundled and finished with another PACKET_DELIMITER (0x00). Within the Netty pipeline, this sequence is transformed into one synthesized packet found in net.minecraft.network.protocol.game.ClientboundBundlePacket, which is essentially just a list of packets. At the stage at which ProtocolLib injects into the clientbound netty pipeline, this packet has not been unpacked yet. Thus, we need to handle the ClientboundBundlePacket, which unfortunately is not registered in ProtocolLib. The fact that two different classes map to the same packet currently requires a dirty remapping in the packet structure modifier.
2023-03-26 04:08:31 +02:00
|
|
|
import com.comphenix.protocol.utility.*;
|
2022-03-08 04:09:04 +01:00
|
|
|
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
|
|
|
import io.netty.channel.Channel;
|
|
|
|
import io.netty.channel.ChannelHandler;
|
2022-08-10 22:50:33 +02:00
|
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
|
|
import io.netty.channel.EventLoop;
|
2022-03-08 04:09:04 +01:00
|
|
|
import io.netty.util.AttributeKey;
|
|
|
|
import org.bukkit.Server;
|
|
|
|
import org.bukkit.entity.Player;
|
|
|
|
|
|
|
|
public class NettyChannelInjector implements Injector {
|
|
|
|
|
2023-05-12 16:35:34 +02:00
|
|
|
// an accessor used when we're unable to retrieve the actual packet field in an outbound packet send
|
|
|
|
private static final FieldAccessor NO_OP_ACCESSOR = new FieldAccessor() {
|
|
|
|
@Override
|
|
|
|
public Object get(Object instance) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void set(Object instance, Object value) {
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Field getField() {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private static final String INTERCEPTOR_NAME = "protocol_lib_inbound_interceptor";
|
|
|
|
private static final String WIRE_PACKET_ENCODER_NAME = "protocol_lib_wire_packet_encoder";
|
|
|
|
|
|
|
|
// all registered channel handlers to easier make sure we unregister them all from the pipeline
|
|
|
|
private static final String[] PROTOCOL_LIB_HANDLERS = new String[]{
|
|
|
|
WIRE_PACKET_ENCODER_NAME, INTERCEPTOR_NAME
|
|
|
|
};
|
|
|
|
|
|
|
|
private static final ReportType REPORT_CANNOT_SEND_PACKET = new ReportType("Unable to send packet %s to %s");
|
|
|
|
|
|
|
|
private static final WirePacketEncoder WIRE_PACKET_ENCODER = new WirePacketEncoder();
|
|
|
|
private static final Map<Class<?>, FieldAccessor> PACKET_ACCESSORS = new ConcurrentHashMap<>(16, 0.9f);
|
|
|
|
|
|
|
|
private static final Class<?> LOGIN_PACKET_START_CLASS = PacketType.Login.Client.START.getPacketClass();
|
|
|
|
private static final Class<?> PACKET_PROTOCOL_CLASS = PacketType.Handshake.Client.SET_PROTOCOL.getPacketClass();
|
|
|
|
|
|
|
|
private static final AttributeKey<Integer> PROTOCOL_VERSION = AttributeKey.valueOf(getRandomKey());
|
|
|
|
private static final AttributeKey<NettyChannelInjector> INJECTOR = AttributeKey.valueOf(getRandomKey());
|
|
|
|
|
|
|
|
// lazy initialized fields, if we don't need them we don't bother about them
|
|
|
|
private static FieldAccessor LOGIN_PROFILE_ACCESSOR;
|
|
|
|
private static FieldAccessor PROTOCOL_VERSION_ACCESSOR;
|
|
|
|
|
|
|
|
// bukkit stuff
|
|
|
|
private final Server server;
|
|
|
|
|
|
|
|
// protocol lib stuff we need
|
|
|
|
private final ErrorReporter errorReporter;
|
|
|
|
private final NetworkProcessor networkProcessor;
|
|
|
|
|
|
|
|
// references
|
|
|
|
private final Object networkManager;
|
|
|
|
private final Channel wrappedChannel;
|
|
|
|
private final ChannelListener channelListener;
|
|
|
|
private final InjectionFactory injectionFactory;
|
|
|
|
|
|
|
|
private final FieldAccessor channelField;
|
|
|
|
|
|
|
|
// packet marking
|
|
|
|
private final Map<Object, NetworkMarker> savedMarkers = new WeakHashMap<>(16, 0.9f);
|
|
|
|
private final Set<Object> skippedPackets = ConcurrentHashMap.newKeySet();
|
|
|
|
protected final ThreadLocal<Boolean> processedPackets = ThreadLocal.withInitial(() -> Boolean.FALSE);
|
|
|
|
|
|
|
|
// status of this injector
|
|
|
|
private volatile boolean closed = false;
|
|
|
|
private volatile boolean injected = false;
|
|
|
|
|
|
|
|
// information about the player belonging to this injector
|
|
|
|
private String playerName;
|
|
|
|
private Player resolvedPlayer;
|
|
|
|
|
|
|
|
// lazy initialized fields, if we don't need them we don't bother about them
|
|
|
|
private Object playerConnection;
|
|
|
|
private FieldAccessor protocolAccessor;
|
|
|
|
|
|
|
|
public NettyChannelInjector(
|
|
|
|
Player player,
|
|
|
|
Server server,
|
|
|
|
Object netManager,
|
|
|
|
Channel channel,
|
|
|
|
ChannelListener listener,
|
|
|
|
InjectionFactory injector,
|
|
|
|
ErrorReporter errorReporter
|
|
|
|
) {
|
|
|
|
// bukkit stuff
|
|
|
|
this.server = server;
|
|
|
|
this.resolvedPlayer = player;
|
|
|
|
|
|
|
|
// protocol lib stuff
|
|
|
|
this.errorReporter = errorReporter;
|
|
|
|
this.networkProcessor = new NetworkProcessor(errorReporter);
|
|
|
|
|
|
|
|
// references
|
|
|
|
this.networkManager = netManager;
|
|
|
|
this.wrappedChannel = channel;
|
|
|
|
this.channelListener = listener;
|
|
|
|
this.injectionFactory = injector;
|
|
|
|
|
|
|
|
// register us into the channel
|
|
|
|
this.wrappedChannel.attr(INJECTOR).set(this);
|
|
|
|
|
|
|
|
// read the channel field from the network manager given to this method
|
|
|
|
// we re-read this field every time as plugins/spigot forks might give us different network manager types
|
|
|
|
Field channelField = FuzzyReflection.fromObject(netManager, true).getField(FuzzyFieldContract.newBuilder()
|
|
|
|
.typeExact(Channel.class)
|
|
|
|
.banModifier(Modifier.STATIC)
|
|
|
|
.build());
|
|
|
|
this.channelField = Accessors.getFieldAccessor(channelField);
|
|
|
|
|
|
|
|
// hook here into the close future to be 100% sure that this injector gets closed when the channel we wrap gets closed
|
|
|
|
// normally we listen to the disconnect event, but there is a very small period of time, between the login and actual
|
|
|
|
// join that is not covered by the disconnect event and may lead to unexpected injector states...
|
|
|
|
this.wrappedChannel.closeFuture().addListener(future -> this.close());
|
|
|
|
}
|
|
|
|
|
|
|
|
static NettyChannelInjector findInjector(Channel channel) {
|
|
|
|
return channel.attr(INJECTOR).get();
|
|
|
|
}
|
|
|
|
|
|
|
|
static Object findChannelHandler(Channel channel, Class<?> type) {
|
|
|
|
for (Entry<String, ChannelHandler> entry : channel.pipeline()) {
|
|
|
|
if (type.isAssignableFrom(entry.getValue().getClass())) {
|
|
|
|
return entry.getValue();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static String getRandomKey() {
|
|
|
|
return "ProtocolLib-" + ThreadLocalRandom.current().nextLong();
|
|
|
|
}
|
|
|
|
|
|
|
|
private static boolean hasProtocolLibHandler(Channel channel) {
|
|
|
|
for (String handler : PROTOCOL_LIB_HANDLERS) {
|
|
|
|
if (channel.pipeline().get(handler) != null) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getProtocolVersion() {
|
|
|
|
Integer protocolVersion = this.wrappedChannel.attr(PROTOCOL_VERSION).get();
|
|
|
|
return protocolVersion == null ? MinecraftProtocolVersion.getCurrentVersion() : protocolVersion;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean inject() {
|
|
|
|
// we only do this on the channel event loop to prevent blocking the main server thread
|
|
|
|
// and to be sure that the netty pipeline view we get is up-to-date
|
|
|
|
if (this.wrappedChannel.eventLoop().inEventLoop()) {
|
|
|
|
// ensure that we should actually inject into the channel
|
|
|
|
if (this.closed || this.wrappedChannel instanceof ByteBuddyGenerated || !this.wrappedChannel.isActive()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check here if we need to rewrite the channel field and do so
|
|
|
|
// minecraft overrides the channel field when the channel actually becomes active, so we need to ensure that our
|
|
|
|
// proxied channel is always on that field - therefore this rewrite is event before we check if we're already
|
|
|
|
// injected into the channel
|
|
|
|
this.rewriteChannelField();
|
|
|
|
|
|
|
|
// check if we already injected into the channel
|
|
|
|
if (hasProtocolLibHandler(this.wrappedChannel)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// inject our handlers
|
|
|
|
this.wrappedChannel.pipeline().addAfter("encoder", WIRE_PACKET_ENCODER_NAME, WIRE_PACKET_ENCODER);
|
|
|
|
this.wrappedChannel.pipeline().addAfter(
|
|
|
|
"decoder",
|
|
|
|
INTERCEPTOR_NAME,
|
|
|
|
new InboundPacketInterceptor(this, this.channelListener));
|
|
|
|
|
|
|
|
this.injected = true;
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
// re-run in event loop, return false as we cannot be sure if the injection actually worked
|
|
|
|
this.ensureInEventLoop(this::inject);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void uninject() {
|
|
|
|
// ensure that we injected into the channel before trying to remove anything from it
|
|
|
|
if (this.injected) {
|
|
|
|
// uninject on the event loop to ensure the instant visibility of the change and prevent blocks of other threads
|
|
|
|
if (this.wrappedChannel.eventLoop().inEventLoop()) {
|
|
|
|
this.injected = false;
|
|
|
|
|
|
|
|
// remove known references to us
|
|
|
|
this.wrappedChannel.attr(INJECTOR).remove();
|
|
|
|
this.channelField.set(this.networkManager, this.wrappedChannel);
|
|
|
|
|
|
|
|
for (String handler : PROTOCOL_LIB_HANDLERS) {
|
|
|
|
try {
|
|
|
|
this.wrappedChannel.pipeline().remove(handler);
|
|
|
|
} catch (NoSuchElementException ignored) {
|
|
|
|
// ignore that one, probably an edge case
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.ensureInEventLoop(this::uninject);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void close() {
|
|
|
|
// ensure that the injector wasn't close before
|
|
|
|
if (!this.closed) {
|
|
|
|
this.closed = true;
|
|
|
|
|
|
|
|
// remove all of our references from the channel
|
|
|
|
this.uninject();
|
|
|
|
|
|
|
|
// cleanup
|
|
|
|
this.savedMarkers.clear();
|
|
|
|
this.skippedPackets.clear();
|
|
|
|
|
|
|
|
// wipe this injector completely
|
|
|
|
this.injectionFactory.invalidate(this.getPlayer(), this.playerName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void sendServerPacket(Object packet, NetworkMarker marker, boolean filtered) {
|
|
|
|
// do not send the packet if this injector was already closed / is not injected yet
|
|
|
|
if (this.closed || !this.injected) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// register the packet as filtered if we shouldn't post it to any listener
|
|
|
|
if (!filtered) {
|
|
|
|
this.skippedPackets.add(packet);
|
|
|
|
}
|
|
|
|
|
|
|
|
// save the given packet marker and send the packet
|
|
|
|
this.saveMarker(packet, marker);
|
|
|
|
try {
|
|
|
|
if (this.resolvedPlayer instanceof ByteBuddyGenerated) {
|
|
|
|
MinecraftMethods.getNetworkManagerHandleMethod().invoke(this.networkManager, packet);
|
|
|
|
} else {
|
|
|
|
// ensure that the player is properly connected before sending
|
|
|
|
Object playerConnection = this.getPlayerConnection();
|
|
|
|
if (playerConnection != null) {
|
|
|
|
MinecraftMethods.getSendPacketMethod().invoke(playerConnection, packet);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (Exception exception) {
|
|
|
|
this.errorReporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_SEND_PACKET)
|
|
|
|
.messageParam(packet, this.playerName)
|
|
|
|
.error(exception)
|
|
|
|
.build());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void receiveClientPacket(Object packet) {
|
|
|
|
// do not do that if we're not injected or this injector was closed
|
|
|
|
if (this.closed || !this.injected) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
Runnable receiveAction = () -> {
|
|
|
|
try {
|
|
|
|
// try to invoke the method, this should normally not fail
|
|
|
|
MinecraftMethods.getNetworkManagerReadPacketMethod().invoke(this.networkManager, null, packet);
|
|
|
|
} catch (Exception exception) {
|
|
|
|
// 99% the user gave wrong information to the server
|
|
|
|
this.errorReporter.reportMinimal(this.injectionFactory.getPlugin(), "receiveClientPacket", exception);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// execute the action on the event loop rather than any thread which we should potentially not block
|
|
|
|
if (this.wrappedChannel.eventLoop().inEventLoop()) {
|
|
|
|
receiveAction.run();
|
|
|
|
} else {
|
|
|
|
this.ensureInEventLoop(receiveAction);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Protocol getCurrentProtocol() {
|
|
|
|
// ensure that the accessor to the protocol field is available
|
|
|
|
if (this.protocolAccessor == null) {
|
|
|
|
this.protocolAccessor = Accessors.getFieldAccessor(
|
|
|
|
this.networkManager.getClass(),
|
|
|
|
MinecraftReflection.getEnumProtocolClass(),
|
|
|
|
true);
|
|
|
|
}
|
|
|
|
|
|
|
|
Object nmsProtocol = this.protocolAccessor.get(this.networkManager);
|
|
|
|
return Protocol.fromVanilla((Enum<?>) nmsProtocol);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public NetworkMarker getMarker(Object packet) {
|
|
|
|
return this.savedMarkers.get(packet);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void saveMarker(Object packet, NetworkMarker marker) {
|
|
|
|
if (marker != null && !this.closed) {
|
|
|
|
this.savedMarkers.put(packet, marker);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Player getPlayer() {
|
|
|
|
// if the player was already resolved there is no need to do further lookups
|
|
|
|
if (this.resolvedPlayer != null) {
|
|
|
|
return this.resolvedPlayer;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if the name of the player is already known to the injector
|
|
|
|
if (this.playerName != null) {
|
|
|
|
this.resolvedPlayer = this.server.getPlayerExact(this.playerName);
|
|
|
|
}
|
|
|
|
|
|
|
|
// either we resolved it or we didn't...
|
|
|
|
return this.resolvedPlayer;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void setPlayer(Player player) {
|
|
|
|
this.resolvedPlayer = player;
|
|
|
|
this.playerName = player.getName();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void disconnect(String message) {
|
|
|
|
// we're still during pre-login, just close the connection
|
|
|
|
if (this.playerConnection == null || this.resolvedPlayer instanceof ByteBuddyGenerated) {
|
|
|
|
this.wrappedChannel.disconnect();
|
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
// try to call the disconnect method on the player
|
|
|
|
MinecraftMethods.getDisconnectMethod(this.playerConnection.getClass()).invoke(this.playerConnection, message);
|
|
|
|
} catch (Exception exception) {
|
|
|
|
throw new IllegalArgumentException("Unable to disconnect the current injector", exception);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean isInjected() {
|
|
|
|
return this.injected;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean isClosed() {
|
|
|
|
return this.closed;
|
|
|
|
}
|
|
|
|
|
|
|
|
void tryProcessLogin(Object packet) {
|
|
|
|
// check if the given packet is a login packet
|
|
|
|
if (LOGIN_PACKET_START_CLASS != null && LOGIN_PACKET_START_CLASS.equals(packet.getClass())) {
|
|
|
|
if (MinecraftVersion.WILD_UPDATE.atOrAbove()) {
|
|
|
|
// 1.19 removed the profile from the packet and now sends the plain username directly
|
|
|
|
// ensure that the game profile accessor is available
|
|
|
|
if (LOGIN_PROFILE_ACCESSOR == null) {
|
|
|
|
LOGIN_PROFILE_ACCESSOR = Accessors.getFieldAccessor(LOGIN_PACKET_START_CLASS, String.class, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// get the username from the packet
|
|
|
|
String username = (String) LOGIN_PROFILE_ACCESSOR.get(packet);
|
|
|
|
|
|
|
|
// cache the injector and the player name
|
|
|
|
this.playerName = username;
|
|
|
|
this.injectionFactory.cacheInjector(username, this);
|
|
|
|
} else {
|
|
|
|
// ensure that the game profile accessor is available
|
|
|
|
if (LOGIN_PROFILE_ACCESSOR == null) {
|
|
|
|
LOGIN_PROFILE_ACCESSOR = Accessors.getFieldAccessor(
|
|
|
|
LOGIN_PACKET_START_CLASS,
|
|
|
|
MinecraftReflection.getGameProfileClass(),
|
|
|
|
true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// the client only sends the name but the server wraps it into a GameProfile, so here we are
|
|
|
|
WrappedGameProfile profile = WrappedGameProfile.fromHandle(LOGIN_PROFILE_ACCESSOR.get(packet));
|
|
|
|
|
|
|
|
// cache the injector and the player name
|
|
|
|
this.playerName = profile.getName();
|
|
|
|
this.injectionFactory.cacheInjector(profile.getName(), this);
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// protocol version begin
|
|
|
|
if (PACKET_PROTOCOL_CLASS != null && PACKET_PROTOCOL_CLASS.equals(packet.getClass())) {
|
|
|
|
// ensure the protocol version accessor is available
|
|
|
|
if (PROTOCOL_VERSION_ACCESSOR == null) {
|
|
|
|
try {
|
|
|
|
Field ver = FuzzyReflection.fromClass(PACKET_PROTOCOL_CLASS, true).getField(FuzzyFieldContract.newBuilder()
|
|
|
|
.banModifier(Modifier.STATIC)
|
|
|
|
.typeExact(int.class)
|
|
|
|
.build());
|
|
|
|
PROTOCOL_VERSION_ACCESSOR = Accessors.getFieldAccessor(ver);
|
|
|
|
} catch (IllegalArgumentException exception) {
|
|
|
|
// unable to resolve that field, continue no-op
|
|
|
|
PROTOCOL_VERSION_ACCESSOR = NO_OP_ACCESSOR;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// read the protocol version from the field if available
|
|
|
|
if (PROTOCOL_VERSION_ACCESSOR != NO_OP_ACCESSOR) {
|
|
|
|
int protocolVersion = (int) PROTOCOL_VERSION_ACCESSOR.get(packet);
|
|
|
|
this.wrappedChannel.attr(PROTOCOL_VERSION).set(protocolVersion);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void rewriteChannelField() {
|
|
|
|
// check if we need to rewrite the channel or if the channel is already correct (prevent wrapping a wrapped channel)
|
|
|
|
Object currentChannel = this.channelField.get(this.networkManager);
|
|
|
|
if (currentChannel instanceof NettyChannelProxy) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// the field is not correct, rewrite now to our handler
|
|
|
|
Channel ch = new NettyChannelProxy(this.wrappedChannel, new NettyEventLoopProxy(this.wrappedChannel.eventLoop(), this) {
|
|
|
|
@Override
|
|
|
|
protected Runnable doProxyRunnable(Runnable original) {
|
|
|
|
return NettyChannelInjector.this.processOutbound(original);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected <T> Callable<T> doProxyCallable(Callable<T> original) {
|
|
|
|
return NettyChannelInjector.this.processOutbound(original);
|
|
|
|
}
|
|
|
|
}, this);
|
|
|
|
this.channelField.set(this.networkManager, ch);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void ensureInEventLoop(Runnable runnable) {
|
|
|
|
this.ensureInEventLoop(this.wrappedChannel.eventLoop(), runnable);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void ensureInEventLoop(EventLoop eventLoop, Runnable runnable) {
|
|
|
|
if (eventLoop.inEventLoop()) {
|
|
|
|
runnable.run();
|
|
|
|
} else {
|
|
|
|
eventLoop.execute(runnable);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void processInboundPacket(ChannelHandlerContext ctx, Object packet, Class<?> packetClass) {
|
|
|
|
if (this.channelListener.hasMainThreadListener(packetClass) && !this.server.isPrimaryThread()) {
|
|
|
|
// not on the main thread but we are required to be - re-schedule the packet on the main thread
|
2023-07-04 03:45:47 +02:00
|
|
|
ProtocolLibrary.getScheduler().runTask(
|
2023-05-12 16:35:34 +02:00
|
|
|
() -> this.processInboundPacket(ctx, packet, packetClass));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// call packet handlers, a null result indicates that we shouldn't change anything
|
|
|
|
PacketEvent interceptionResult = this.channelListener.onPacketReceiving(this, packet, null);
|
|
|
|
if (interceptionResult == null) {
|
|
|
|
this.ensureInEventLoop(ctx.channel().eventLoop(), () -> ctx.fireChannelRead(packet));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// fire the intercepted packet down the pipeline if it wasn't cancelled
|
|
|
|
if (!interceptionResult.isCancelled()) {
|
|
|
|
this.ensureInEventLoop(
|
|
|
|
ctx.channel().eventLoop(),
|
|
|
|
() -> ctx.fireChannelRead(interceptionResult.getPacket().getHandle()));
|
|
|
|
|
|
|
|
// check if there were any post events added the packet after we fired it down the pipeline
|
|
|
|
// we use this way as we don't want to construct a new network manager accidentally
|
|
|
|
NetworkMarker marker = NetworkMarker.getNetworkMarker(interceptionResult);
|
|
|
|
if (marker != null) {
|
|
|
|
this.networkProcessor.invokePostEvent(interceptionResult, marker);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
<T> T processOutbound(T action) {
|
|
|
|
// get the accessor to the packet field
|
|
|
|
// if we are unable to look up the accessor then just return the runnable, probably nothing of our business
|
|
|
|
FieldAccessor packetAccessor = this.lookupPacketAccessor(action);
|
|
|
|
if (packetAccessor == NO_OP_ACCESSOR) {
|
|
|
|
return action;
|
|
|
|
}
|
|
|
|
|
|
|
|
// get the packet field and ensure that the field is actually present (should always be, just to be sure)
|
|
|
|
Object packet = packetAccessor.get(action);
|
|
|
|
if (packet == null) {
|
|
|
|
return action;
|
|
|
|
}
|
|
|
|
|
|
|
|
// filter out all packets which were explicitly send to not be processed by any event
|
|
|
|
// pre-checking isEmpty will reduce the need of hashing packets which don't override the
|
|
|
|
// hashCode method; this presents calls to the very slow identityHashCode default implementation
|
|
|
|
NetworkMarker marker = this.savedMarkers.isEmpty() ? null : this.savedMarkers.remove(packet);
|
|
|
|
if (!this.skippedPackets.isEmpty() && this.skippedPackets.remove(packet)) {
|
|
|
|
// if a marker was set there might be scheduled packets to execute after the packet send
|
|
|
|
// for this to work we need to proxy the input action to provide access to them
|
|
|
|
if (marker != null) {
|
|
|
|
return this.proxyAction(action, null, marker);
|
|
|
|
}
|
|
|
|
|
|
|
|
// nothing special, just no processing
|
|
|
|
return action;
|
|
|
|
}
|
|
|
|
|
|
|
|
// no listener and no marker - no magic :)
|
|
|
|
if (!this.channelListener.hasListener(packet.getClass()) && marker == null && !MinecraftReflection.isBundlePacket(packet.getClass())) {
|
|
|
|
return action;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ensure that we are on the main thread if we need to
|
|
|
|
if (this.channelListener.hasMainThreadListener(packet.getClass()) && !this.server.isPrimaryThread()) {
|
|
|
|
// not on the main thread but we are required to be - re-schedule the packet on the main thread
|
2023-07-04 03:45:47 +02:00
|
|
|
ProtocolLibrary.getScheduler().runTask(
|
2023-05-12 16:35:34 +02:00
|
|
|
() -> this.sendServerPacket(packet, null, true));
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// call all listeners which are listening to the outbound packet, if any
|
|
|
|
// null indicates that no listener was affected by the packet, meaning that we can directly send the original packet
|
|
|
|
PacketEvent event = this.channelListener.onPacketSending(this, packet, marker);
|
|
|
|
if (event == null) {
|
|
|
|
return action;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if the event wasn't cancelled by this action we must recheck if the packet changed during the method call
|
|
|
|
if (!event.isCancelled()) {
|
|
|
|
// rewrite the packet in the given action if the packet was changed during the event call
|
|
|
|
Object interceptedPacket = event.getPacket().getHandle();
|
|
|
|
if (interceptedPacket != packet) {
|
|
|
|
packetAccessor.set(action, interceptedPacket);
|
|
|
|
}
|
|
|
|
|
|
|
|
// this is essential to do this way as a call to getMarker on the event will construct a new marker instance if needed
|
|
|
|
// we just want to know here if there is a marker to proceed correctly
|
|
|
|
// if the marker is null we can just schedule the action as we don't need to do anything after the packet was sent
|
|
|
|
NetworkMarker eventMarker = NetworkMarker.getNetworkMarker(event);
|
|
|
|
if (eventMarker == null) {
|
|
|
|
return action;
|
|
|
|
}
|
|
|
|
|
|
|
|
// we need to wrap the action to call the listeners set in the marker
|
|
|
|
return this.proxyAction(action, event, eventMarker);
|
|
|
|
}
|
|
|
|
|
|
|
|
// return null if the event was cancelled to schedule a no-op event
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressWarnings("unchecked")
|
|
|
|
private <T> T proxyAction(T action, PacketEvent event, NetworkMarker marker) {
|
|
|
|
// hack - we only know that the given action is either a runnable or callable, but we need to work out which thing
|
|
|
|
// it is exactly to proceed correctly here.
|
|
|
|
if (action instanceof Runnable) {
|
|
|
|
// easier thing to do - just wrap the runnable in a new one
|
|
|
|
return (T) (Runnable) () -> {
|
|
|
|
// notify the outbound handler that the packets are processed
|
|
|
|
this.processedPackets.set(Boolean.TRUE);
|
|
|
|
// execute the action & invoke the post event
|
|
|
|
((Runnable) action).run();
|
|
|
|
this.networkProcessor.invokePostEvent(event, marker);
|
|
|
|
};
|
|
|
|
} else if (action instanceof Callable<?>) {
|
|
|
|
// okay this is a bit harder now - we need to wrap the action and return the value of it
|
|
|
|
return (T) (Callable<Object>) () -> {
|
|
|
|
// notify the outbound handler that the packets are processed
|
|
|
|
this.processedPackets.set(Boolean.TRUE);
|
|
|
|
// execute the action & invoke the post event
|
|
|
|
Object value = ((Callable<Object>) action).call();
|
|
|
|
this.networkProcessor.invokePostEvent(event, marker);
|
|
|
|
return value;
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
throw new IllegalStateException("Unexpected input action of type " + action.getClass());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private FieldAccessor lookupPacketAccessor(Object action) {
|
|
|
|
return PACKET_ACCESSORS.computeIfAbsent(action.getClass(), clazz -> {
|
|
|
|
try {
|
|
|
|
// find the field
|
|
|
|
Field packetField = FuzzyReflection.fromClass(clazz, true).getField(FuzzyFieldContract
|
|
|
|
.newBuilder()
|
|
|
|
.typeSuperOf(MinecraftReflection.getPacketClass())
|
|
|
|
.build());
|
|
|
|
return Accessors.getFieldAccessor(packetField);
|
|
|
|
} catch (IllegalArgumentException exception) {
|
|
|
|
// no such field found :(
|
|
|
|
return NO_OP_ACCESSOR;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private Object getPlayerConnection() {
|
|
|
|
// resolve the player connection if needed
|
|
|
|
if (this.playerConnection == null) {
|
|
|
|
Player target = this.getPlayer();
|
|
|
|
if (target == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.playerConnection = MinecraftFields.getPlayerConnection(target);
|
|
|
|
}
|
|
|
|
|
|
|
|
// cannot be null at this point
|
|
|
|
return this.playerConnection;
|
|
|
|
}
|
|
|
|
|
|
|
|
public Channel getWrappedChannel() {
|
|
|
|
return this.wrappedChannel;
|
|
|
|
}
|
2022-03-08 04:09:04 +01:00
|
|
|
}
|