Last active
January 25, 2023 16:49
-
-
Save BlackOfWorld/7cad88c33bbdc1cfc2a0c8575a369fc8 to your computer and use it in GitHub Desktop.
TinyProtocol fixed for 1.17+
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class BukkitReflection { | |
public static ServerPlayer getServerPlayer(Player p) { | |
var m = Reflection.getMethodCached("{obc}.entity.CraftPlayer", "getHandle"); | |
return invoke(m, p); | |
} | |
private static <T> T invoke(java.lang.reflect.Method m, Object instance, Object... args) { | |
try { | |
return (T) m.invoke(instance, args); | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.reflect.*; | |
import java.security.AccessController; | |
import java.security.PrivilegedAction; | |
// Use Kristian's (Original author of TinyProtocol) Reflection class, it's good and has everything you'll need | |
public class Reflection { | |
public static < T extends AccessibleObject > T setAccessible(T object, boolean access) { | |
AccessController.doPrivileged((PrivilegedAction)() - > { | |
object.setAccessible(access); | |
return null; | |
}); | |
return object; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.google.common.collect.Lists; | |
import com.google.common.collect.MapMaker; | |
import com.mojang.authlib.GameProfile; | |
import io.netty.channel.*; | |
import net.blackofworld.sneakybastard.Utils.BukkitReflection; | |
import net.blackofworld.sneakybastard.Utils.Reflection; | |
import net.minecraft.network.protocol.login.ClientboundGameProfilePacket; | |
import net.minecraft.server.MinecraftServer; | |
import net.minecraft.server.network.ServerConnectionListener; | |
import net.minecraft.server.network.ServerGamePacketListenerImpl; | |
import org.bukkit.entity.Player; | |
import org.bukkit.event.EventHandler; | |
import org.bukkit.event.EventPriority; | |
import org.bukkit.event.HandlerList; | |
import org.bukkit.event.Listener; | |
import org.bukkit.event.player.PlayerJoinEvent; | |
import org.bukkit.event.server.PluginDisableEvent; | |
import org.bukkit.plugin.Plugin; | |
import org.bukkit.scheduler.BukkitRunnable; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.InvocationTargetException; | |
import java.lang.reflect.Modifier; | |
import java.util.*; | |
import java.util.concurrent.atomic.AtomicInteger; | |
import java.util.logging.Level; | |
/** | |
* Represents a very tiny alternative to ProtocolLib. | |
* <p> | |
* It now supports intercepting packets during login and status ping (such as OUT_SERVER_PING)! | |
* | |
* @author Kristian | |
*/ | |
public abstract class TinyProtocol { | |
private static final AtomicInteger ID = new AtomicInteger(0); | |
private static Field getNetworkMarkers = null; | |
private static Field getChannels = null; | |
protected volatile boolean closed; | |
protected Plugin plugin; | |
// Speedup channel lookup | |
private Map < String, Channel > channelLookup = new MapMaker().weakValues().makeMap(); | |
private Listener listener; | |
// Channels that have already been removed | |
private Set < Channel > uninjectedChannels = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); | |
// List of network markers | |
private List < Object > networkManagers; | |
// Injected channel handlers | |
private List < Channel > serverChannels = Lists.newArrayList(); | |
private ChannelInboundHandlerAdapter serverChannelHandler; | |
private ChannelInitializer < Channel > beginInitProtocol; | |
private ChannelInitializer < Channel > endInitProtocol; | |
// Current handler name | |
private String handlerName; | |
static { | |
var fields = ServerConnectionListener.class.getDeclaredFields(); | |
for (var field: fields) { | |
var mod = field.getModifiers(); | |
if (Modifier.isFinal(mod) && Modifier.isPrivate(mod) && field.getType().equals(List.class)) { | |
getChannels = field; | |
Reflection.setAccessible(getChannels, true); | |
continue; | |
} else if (getChannels != null && Modifier.isFinal(mod) && field.getType().equals(List.class)) { | |
getNetworkMarkers = field; | |
Reflection.setAccessible(getNetworkMarkers, true); | |
break; | |
} | |
} | |
} | |
/** | |
* Construct a new instance of TinyProtocol, and start intercepting packets for all connected clients and future clients. | |
* <p> | |
* You can construct multiple instances per plugin. | |
* | |
* @param plugin - the plugin. | |
*/ | |
public TinyProtocol(final Plugin plugin) { | |
this.plugin = plugin; | |
// Compute handler name | |
this.handlerName = getHandlerName(); | |
// Prepare existing players | |
registerBukkitEvents(); | |
try { | |
try { | |
registerChannelHandler(); | |
registerPlayers(plugin); | |
} catch (InvocationTargetException e) { | |
e.printStackTrace(); | |
} catch (IllegalAccessException e) { | |
e.printStackTrace(); | |
} | |
} catch (IllegalArgumentException ex) { | |
// Damn you, late bind | |
new BukkitRunnable() { | |
@Override | |
public void run() { | |
try { | |
registerChannelHandler(); | |
registerPlayers(plugin); | |
} catch (InvocationTargetException e) { | |
e.printStackTrace(); | |
} catch (IllegalAccessException e) { | |
e.printStackTrace(); | |
} | |
} | |
}.runTask(plugin); | |
} | |
} | |
private void createServerChannelHandler() { | |
// Handle connected channels | |
endInitProtocol = new ChannelInitializer < Channel > () { | |
@Override | |
protected void initChannel(Channel channel) throws Exception { | |
try { | |
// This can take a while, so we need to stop the main thread from interfering | |
synchronized(networkManagers) { | |
// Stop injecting channels | |
if (!closed) { | |
channel.eventLoop().submit(() - > injectChannelInternal(channel)); | |
} | |
} | |
} catch (Exception e) {} | |
} | |
}; | |
// This is executed before Minecraft's channel handler | |
beginInitProtocol = new ChannelInitializer < Channel > () { | |
@Override | |
protected void initChannel(Channel channel) throws Exception { | |
channel.pipeline().addLast(endInitProtocol); | |
} | |
}; | |
serverChannelHandler = new ChannelInboundHandlerAdapter() { | |
@Override | |
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { | |
Channel channel = (Channel) msg; | |
// Prepare to initialize the channel | |
channel.pipeline().addFirst(beginInitProtocol); | |
//channel.pipeline().addFirst(customPacketHandler); // TODO: work on this | |
ctx.fireChannelRead(msg); | |
} | |
}; | |
} | |
/** | |
* Register bukkit events. | |
*/ | |
private void registerBukkitEvents() { | |
listener = new Listener() { | |
@EventHandler(priority = EventPriority.LOWEST) | |
public final void onPlayerJoin(PlayerJoinEvent e) throws InvocationTargetException, IllegalAccessException { | |
if (closed) | |
return; | |
Channel channel = getChannel(e.getPlayer()); | |
// Don't inject players that have been explicitly uninjected | |
if (!uninjectedChannels.contains(channel)) { | |
injectPlayer(e.getPlayer()); | |
} | |
} | |
@EventHandler | |
public final void onPluginDisable(PluginDisableEvent e) throws InvocationTargetException, IllegalAccessException { | |
if (e.getPlugin().equals(plugin)) { | |
close(); | |
} | |
} | |
}; | |
plugin.getServer().getPluginManager().registerEvents(listener, plugin); | |
} | |
@SuppressWarnings("unchecked") | |
private void registerChannelHandler() throws InvocationTargetException, IllegalAccessException { | |
ServerConnectionListener serverConnection = MinecraftServer.getServer().getConnection(); | |
boolean looking = true; | |
// We need to synchronize against this list | |
networkManagers = (List < Object > ) getNetworkMarkers.get(serverConnection); | |
createServerChannelHandler(); | |
// Find the correct list, or implicitly throw an exception | |
for (int i = 0; looking; i++) { | |
List < Object > list = (List < Object > ) getChannels.get(serverConnection); | |
for (Object item: list) { | |
if (!(item instanceof ChannelFuture)) | |
break; | |
// Channel future that contains the server connection | |
Channel serverChannel = ((ChannelFuture) item).channel(); | |
serverChannels.add(serverChannel); | |
serverChannel.pipeline().addFirst(serverChannelHandler); | |
looking = false; | |
} | |
} | |
} | |
private void unregisterChannelHandler() { | |
if (serverChannelHandler == null) | |
return; | |
for (Channel serverChannel: serverChannels) { | |
final ChannelPipeline pipeline = serverChannel.pipeline(); | |
// Remove channel handler | |
serverChannel.eventLoop().execute(new Runnable() { | |
@Override | |
public void run() { | |
try { | |
pipeline.remove(serverChannelHandler); | |
} catch (NoSuchElementException e) { | |
// That's fine | |
} | |
} | |
}); | |
} | |
} | |
private void registerPlayers(Plugin plugin) throws InvocationTargetException, IllegalAccessException { | |
for (Player player: plugin.getServer().getOnlinePlayers()) { | |
injectPlayer(player); | |
} | |
} | |
/** | |
* Invoked when the server is starting to send a packet to a player. | |
* <p> | |
* Note that this is not executed on the main thread. | |
* | |
* @param receiver - the receiving player, NULL for early login/status packets. | |
* @param channel - the channel that received the packet. Never NULL. | |
* @param packet - the packet being sent. | |
* @return The packet to send instead, or NULL to cancel the transmission. | |
*/ | |
public Object onPacketOutAsync(Player receiver, Channel channel, Object packet) { | |
return packet; | |
} | |
/** | |
* Invoked when the server has received a packet from a given player. | |
* <p> | |
* Use {@link Channel#remoteAddress()} to get the remote address of the client. | |
* | |
* @param sender - the player that sent the packet, NULL for early login/status packets. | |
* @param channel - channel that received the packet. Never NULL. | |
* @param packet - the packet being received. | |
* @return The packet to recieve instead, or NULL to cancel. | |
*/ | |
public Object onPacketInAsync(Player sender, Channel channel, Object packet) { | |
return packet; | |
} | |
/** | |
* Send a packet to a particular player. | |
* <p> | |
* Note that {@link #onPacketOutAsync(Player, Channel, Object)} will be invoked with this packet. | |
* | |
* @param player - the destination player. | |
* @param packet - the packet to send. | |
*/ | |
public void sendPacket(Player player, Object packet) throws InvocationTargetException, IllegalAccessException { | |
sendPacket(getChannel(player), packet); | |
} | |
/** | |
* Send a packet to a particular client. | |
* <p> | |
* Note that {@link #onPacketOutAsync(Player, Channel, Object)} will be invoked with this packet. | |
* | |
* @param channel - client identified by a channel. | |
* @param packet - the packet to send. | |
*/ | |
public void sendPacket(Channel channel, Object packet) { | |
channel.pipeline().writeAndFlush(packet); | |
} | |
/** | |
* Pretend that a given packet has been received from a player. | |
* <p> | |
* Note that {@link #onPacketInAsync(Player, Channel, Object)} will be invoked with this packet. | |
* | |
* @param player - the player that sent the packet. | |
* @param packet - the packet that will be received by the server. | |
*/ | |
public void receivePacket(Player player, Object packet) throws InvocationTargetException, IllegalAccessException { | |
receivePacket(getChannel(player), packet); | |
} | |
/** | |
* Pretend that a given packet has been received from a given client. | |
* <p> | |
* Note that {@link #onPacketInAsync(Player, Channel, Object)} will be invoked with this packet. | |
* | |
* @param channel - client identified by a channel. | |
* @param packet - the packet that will be received by the server. | |
*/ | |
public void receivePacket(Channel channel, Object packet) { | |
channel.pipeline().context("encoder").fireChannelRead(packet); | |
} | |
/** | |
* Retrieve the name of the channel injector, default implementation is "tiny-" + plugin name + "-" + a unique ID. | |
* <p> | |
* Note that this method will only be invoked once. It is no loger necessary to override this to support multiple instances. | |
* | |
* @return A unique channel handler name. | |
*/ | |
protected String getHandlerName() { | |
return plugin.getName() + "-" + ID.incrementAndGet(); | |
} | |
/** | |
* Add a custom channel handler to the given player's channel pipeline, allowing us to intercept sent and received packets. | |
* <p> | |
* This will automatically be called when a player has logged in. | |
* | |
* @param player - the player to inject. | |
*/ | |
public void injectPlayer(Player player) throws InvocationTargetException, IllegalAccessException { | |
injectChannelInternal(getChannel(player)).player = player; | |
} | |
/** | |
* Add a custom channel handler to the given channel. | |
* | |
* @param channel - the channel to inject. | |
* @return The intercepted channel, or NULL if it has already been injected. | |
*/ | |
public void injectChannel(Channel channel) { | |
injectChannelInternal(channel); | |
} | |
/** | |
* Add a custom channel handler to the given channel. | |
* | |
* @param channel - the channel to inject. | |
* @return The packet interceptor. | |
*/ | |
private PacketInterceptor injectChannelInternal(Channel channel) { | |
try { | |
PacketInterceptor interceptor = (PacketInterceptor) channel.pipeline().get(handlerName); | |
// Inject our packet interceptor | |
if (interceptor == null) { | |
interceptor = new PacketInterceptor(); | |
channel.pipeline().addBefore("packet_handler", handlerName, interceptor); | |
uninjectedChannels.remove(channel); | |
} | |
return interceptor; | |
} catch (IllegalArgumentException e) { | |
// Try again | |
return (PacketInterceptor) channel.pipeline().get(handlerName); | |
} | |
} | |
/** | |
* Retrieve the Netty channel associated with a player. This is cached. | |
* | |
* @param player - the player. | |
* @return The Netty channel. | |
*/ | |
public Channel getChannel(Player player) throws IllegalAccessException, InvocationTargetException { | |
Channel channel = channelLookup.get(player.getName()); | |
// Lookup channel again | |
if (channel == null) { | |
ServerGamePacketListenerImpl connection = BukkitReflection.getServerPlayer(player).connection; | |
channelLookup.put(player.getName(), channel = connection.connection.channel); | |
} | |
return channel; | |
} | |
/** | |
* Uninject a specific player. | |
* | |
* @param player - the injected player. | |
*/ | |
public void uninjectPlayer(Player player) throws InvocationTargetException, IllegalAccessException { | |
uninjectChannel(getChannel(player)); | |
} | |
/** | |
* Uninject a specific channel. | |
* <p> | |
* This will also disable the automatic channel injection that occurs when a player has properly logged in. | |
* | |
* @param channel - the injected channel. | |
*/ | |
public void uninjectChannel(final Channel channel) { | |
// No need to guard against this if we're closing | |
if (!closed) { | |
uninjectedChannels.add(channel); | |
} | |
// See ChannelInjector in ProtocolLib, line 590 | |
channel.eventLoop().execute(() - > channel.pipeline().remove(handlerName)); | |
} | |
/** | |
* Determine if the given player has been injected by TinyProtocol. | |
* | |
* @param player - the player. | |
* @return TRUE if it is, FALSE otherwise. | |
*/ | |
public boolean hasInjected(Player player) throws InvocationTargetException, IllegalAccessException { | |
return hasInjected(getChannel(player)); | |
} | |
/** | |
* Determine if the given channel has been injected by TinyProtocol. | |
* | |
* @param channel - the channel. | |
* @return TRUE if it is, FALSE otherwise. | |
*/ | |
public boolean hasInjected(Channel channel) { | |
return channel.pipeline().get(handlerName) != null; | |
} | |
/** | |
* Cease listening for packets. This is called automatically when your plugin is disabled. | |
*/ | |
public final void close() throws InvocationTargetException, IllegalAccessException { | |
if (!closed) { | |
closed = true; | |
// Remove our handlers | |
for (Player player: plugin.getServer().getOnlinePlayers()) { | |
uninjectPlayer(player); | |
} | |
// Clean up Bukkit | |
HandlerList.unregisterAll(listener); | |
unregisterChannelHandler(); | |
} | |
} | |
/** | |
* Channel handler that is inserted into the player's channel pipeline, allowing us to intercept sent and received packets. | |
* | |
* @author Kristian | |
*/ | |
private final class PacketInterceptor extends ChannelDuplexHandler { | |
// Updated by the login event | |
public volatile Player player; | |
@Override | |
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { | |
// Intercept channel | |
final Channel channel = ctx.channel(); | |
handleLoginStart(channel, msg); | |
try { | |
msg = onPacketInAsync(player, channel, msg); | |
} catch (Exception e) { | |
plugin.getLogger().log(Level.SEVERE, "Error in onPacketInAsync().", e); | |
} | |
if (msg != null) { | |
super.channelRead(ctx, msg); | |
} | |
} | |
@Override | |
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { | |
try { | |
msg = onPacketOutAsync(player, ctx.channel(), msg); | |
} catch (Exception e) { | |
plugin.getLogger().log(Level.SEVERE, "Error in onPacketOutAsync().", e); | |
} | |
if (msg != null) { | |
super.write(ctx, msg, promise); | |
} | |
} | |
private void handleLoginStart(Channel channel, Object packet) throws IllegalAccessException { | |
if (ClientboundGameProfilePacket.class.isInstance(packet)) { | |
GameProfile profile = ((ClientboundGameProfilePacket) packet).getGameProfile(); | |
channelLookup.put(profile.getName(), channel); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment