Skip to content

Instantly share code, notes, and snippets.

@BlackOfWorld
Last active January 25, 2023 16:49
Show Gist options
  • Select an option

  • Save BlackOfWorld/7cad88c33bbdc1cfc2a0c8575a369fc8 to your computer and use it in GitHub Desktop.

Select an option

Save BlackOfWorld/7cad88c33bbdc1cfc2a0c8575a369fc8 to your computer and use it in GitHub Desktop.

Revisions

  1. BlackOfWorld revised this gist Jan 25, 2023. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions TinyProtocol.java
    Original file line number Diff line number Diff line change
    @@ -13,7 +13,7 @@
    import org.bukkit.event.EventPriority;
    import org.bukkit.event.HandlerList;
    import org.bukkit.event.Listener;
    import org.bukkit.event.player.PlayerLoginEvent;
    import org.bukkit.event.player.PlayerJoinEvent;
    import org.bukkit.event.server.PluginDisableEvent;
    import org.bukkit.plugin.Plugin;
    import org.bukkit.scheduler.BukkitRunnable;
    @@ -162,7 +162,7 @@ private void registerBukkitEvents() {
    listener = new Listener() {

    @EventHandler(priority = EventPriority.LOWEST)
    public final void onPlayerLogin(PlayerLoginEvent e) throws InvocationTargetException, IllegalAccessException {
    public final void onPlayerJoin(PlayerJoinEvent e) throws InvocationTargetException, IllegalAccessException {
    if (closed)
    return;

  2. BlackOfWorld revised this gist Jan 24, 2023. 3 changed files with 59 additions and 34 deletions.
    13 changes: 13 additions & 0 deletions BukkitReflection.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,13 @@
    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);
    }
    }
    }
    14 changes: 14 additions & 0 deletions Reflection.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,14 @@
    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;
    }
    }
    66 changes: 32 additions & 34 deletions TinyProtocol.java
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import com.google.common.collect.Lists;
    import com.google.common.collect.MapMaker;
    import com.mojang.authlib.GameProfile;
    @@ -42,33 +41,33 @@ public abstract class TinyProtocol {
    protected volatile boolean closed;
    protected Plugin plugin;
    // Speedup channel lookup
    private Map<String, Channel> channelLookup = new MapMaker().weakValues().makeMap();
    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());
    private Set < Channel > uninjectedChannels = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap());
    // List of network markers
    private List<Object> networkManagers;
    private List < Object > networkManagers;
    // Injected channel handlers
    private List<Channel> serverChannels = Lists.newArrayList();
    private List < Channel > serverChannels = Lists.newArrayList();
    private ChannelInboundHandlerAdapter serverChannelHandler;
    private ChannelInitializer<Channel> beginInitProtocol;
    private ChannelInitializer<Channel> endInitProtocol;
    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;
    }
    }
    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;
    }
    }
    }

    /**
    @@ -116,26 +115,25 @@ public void run() {

    private void createServerChannelHandler() {
    // Handle connected channels
    endInitProtocol = new ChannelInitializer<Channel>() {
    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) {
    synchronized(networkManagers) {
    // Stop injecting channels
    if (!closed) {
    channel.eventLoop().submit(() -> injectChannelInternal(channel));
    channel.eventLoop().submit(() - > injectChannelInternal(channel));
    }
    }
    } catch (Exception e) {
    }
    } catch (Exception e) {}
    }

    };

    // This is executed before Minecraft's channel handler
    beginInitProtocol = new ChannelInitializer<Channel>() {
    beginInitProtocol = new ChannelInitializer < Channel > () {

    @Override
    protected void initChannel(Channel channel) throws Exception {
    @@ -194,14 +192,14 @@ private void registerChannelHandler() throws InvocationTargetException, IllegalA
    boolean looking = true;

    // We need to synchronize against this list
    networkManagers = (List<Object>) getNetworkMarkers.get(serverConnection);
    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);
    List < Object > list = (List < Object > ) getChannels.get(serverConnection);

    for (Object item : list) {
    for (Object item: list) {
    if (!(item instanceof ChannelFuture))
    break;

    @@ -219,7 +217,7 @@ private void unregisterChannelHandler() {
    if (serverChannelHandler == null)
    return;

    for (Channel serverChannel : serverChannels) {
    for (Channel serverChannel: serverChannels) {
    final ChannelPipeline pipeline = serverChannel.pipeline();

    // Remove channel handler
    @@ -239,7 +237,7 @@ public void run() {
    }

    private void registerPlayers(Plugin plugin) throws InvocationTargetException, IllegalAccessException {
    for (Player player : plugin.getServer().getOnlinePlayers()) {
    for (Player player: plugin.getServer().getOnlinePlayers()) {
    injectPlayer(player);
    }
    }
    @@ -418,7 +416,7 @@ public void uninjectChannel(final Channel channel) {
    }

    // See ChannelInjector in ProtocolLib, line 590
    channel.eventLoop().execute(() -> channel.pipeline().remove(handlerName));
    channel.eventLoop().execute(() - > channel.pipeline().remove(handlerName));
    }

    /**
    @@ -449,7 +447,7 @@ public final void close() throws InvocationTargetException, IllegalAccessExcepti
    closed = true;

    // Remove our handlers
    for (Player player : plugin.getServer().getOnlinePlayers()) {
    for (Player player: plugin.getServer().getOnlinePlayers()) {
    uninjectPlayer(player);
    }

    @@ -500,7 +498,7 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)

    private void handleLoginStart(Channel channel, Object packet) throws IllegalAccessException {
    if (ClientboundGameProfilePacket.class.isInstance(packet)) {
    GameProfile profile = ((ClientboundGameProfilePacket)packet).getGameProfile();
    GameProfile profile = ((ClientboundGameProfilePacket) packet).getGameProfile();
    channelLookup.put(profile.getName(), channel);
    }
    }
  3. BlackOfWorld created this gist Jan 24, 2023.
    508 changes: 508 additions & 0 deletions TinyProtocol.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,508 @@

    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.PlayerLoginEvent;
    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 onPlayerLogin(PlayerLoginEvent 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);
    }
    }
    }
    }