From 0070421549bf1d8567b6c0a4ce4e16769fb70715 Mon Sep 17 00:00:00 2001
From: Outfluencer <git@outfluencer.dev>
Date: Sat, 15 Feb 2025 14:52:29 +1100
Subject: [PATCH] #3776: Expose ChannelInitializerHolder in protocol module

---
 .../java/net/md_5/bungee/api/ProxyServer.java | 53 ++++++++++++++++++
 .../channel/BungeeChannelInitializer.java     | 54 +++++++++++++++++++
 .../protocol/channel/ChannelAcceptor.java     | 16 ++++++
 .../main/java/net/md_5/bungee/BungeeCord.java | 24 ++++++++-
 .../net/md_5/bungee/BungeeServerInfo.java     |  2 +-
 .../java/net/md_5/bungee/ServerConnector.java |  1 +
 .../java/net/md_5/bungee/UserConnection.java  | 20 ++-----
 .../net/md_5/bungee/netty/PipelineUtils.java  | 47 ++++++++++------
 8 files changed, 184 insertions(+), 33 deletions(-)
 create mode 100644 protocol/src/main/java/net/md_5/bungee/protocol/channel/BungeeChannelInitializer.java
 create mode 100644 protocol/src/main/java/net/md_5/bungee/protocol/channel/ChannelAcceptor.java

diff --git a/api/src/main/java/net/md_5/bungee/api/ProxyServer.java b/api/src/main/java/net/md_5/bungee/api/ProxyServer.java
index 1f330bd2..cab7fa04 100644
--- a/api/src/main/java/net/md_5/bungee/api/ProxyServer.java
+++ b/api/src/main/java/net/md_5/bungee/api/ProxyServer.java
@@ -16,6 +16,7 @@ import net.md_5.bungee.api.connection.ProxiedPlayer;
 import net.md_5.bungee.api.plugin.Plugin;
 import net.md_5.bungee.api.plugin.PluginManager;
 import net.md_5.bungee.api.scheduler.TaskScheduler;
+import net.md_5.bungee.protocol.channel.BungeeChannelInitializer;
 
 public abstract class ProxyServer
 {
@@ -311,4 +312,56 @@ public abstract class ProxyServer
      */
     public abstract Title createTitle();
 
+    /**
+     * Get the unsafe methods of this class.
+     *
+     * @return the unsafe method interface
+     */
+    public abstract Unsafe unsafe();
+
+    public interface Unsafe
+    {
+
+        /**
+         * Gets the frontend channel initializer
+         *
+         * @return the frontend channel initializer
+         */
+        BungeeChannelInitializer getFrontendChannelInitializer();
+
+        /**
+         * Set the frontend channel initializer of this proxy
+         *
+         * @param channelInitializer the frontend channelInitializer to set
+         */
+        void setFrontendChannelInitializer(BungeeChannelInitializer channelInitializer);
+
+        /**
+         * Gets the backend channel initializer
+         *
+         * @return the backend channel initializer
+         */
+        BungeeChannelInitializer getBackendChannelInitializer();
+
+        /**
+         * Set the backend channel initializer of this proxy
+         *
+         * @param channelInitializer the backend channelInitializer to set
+         */
+        void setBackendChannelInitializer(BungeeChannelInitializer channelInitializer);
+
+        /**
+         * Gets the server info channel initializer
+         *
+         * @return the server info channel initializer
+         */
+        BungeeChannelInitializer getServerInfoChannelInitializer();
+
+        /**
+         * Set the server info channel initializer of this proxy
+         *
+         * @param channelInitializer the server info channelInitializer to set
+         */
+        void setServerInfoChannelInitializer(BungeeChannelInitializer channelInitializer);
+    }
 }
diff --git a/protocol/src/main/java/net/md_5/bungee/protocol/channel/BungeeChannelInitializer.java b/protocol/src/main/java/net/md_5/bungee/protocol/channel/BungeeChannelInitializer.java
new file mode 100644
index 00000000..7ab45349
--- /dev/null
+++ b/protocol/src/main/java/net/md_5/bungee/protocol/channel/BungeeChannelInitializer.java
@@ -0,0 +1,54 @@
+package net.md_5.bungee.protocol.channel;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelInitializer;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * This class hold a netty channel initializer that calls the given {@link ChannelAcceptor}.
+ * Use {@link BungeeChannelInitializer#create(ChannelAcceptor)} to create a new instance.
+ * <p>
+ * Please note that this API is unsafe and doesn't provide any guarantees about
+ * the stability of the channel pipeline or the API itself. Use at your own
+ * risk.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public abstract class BungeeChannelInitializer
+{
+
+    public abstract ChannelAcceptor getChannelAcceptor();
+
+    public abstract ChannelInitializer<Channel> getChannelInitializer();
+
+    /**
+     * Creates a new instance of BungeeChannelInitializer
+     *
+     * @param acceptor the {@link ChannelAcceptor} that will accept the channel
+     * and initializer the pipeline
+     * @return {@link BungeeChannelInitializer} containing a cached
+     * {@link ChannelInitializer} that will call the acceptor
+     */
+    public static BungeeChannelInitializer create(ChannelAcceptor acceptor)
+    {
+        return new BungeeChannelInitializer()
+        {
+            @Getter
+            private final ChannelAcceptor channelAcceptor = acceptor;
+
+            @Getter // cache the ChannelInitializer
+            private final ChannelInitializer<Channel> channelInitializer = new ChannelInitializer<Channel>()
+            {
+                @Override
+                protected void initChannel(Channel channel) throws Exception
+                {
+                    if ( !getChannelAcceptor().accept( channel ) )
+                    {
+                        channel.close();
+                    }
+                }
+            };
+        };
+    }
+}
diff --git a/protocol/src/main/java/net/md_5/bungee/protocol/channel/ChannelAcceptor.java b/protocol/src/main/java/net/md_5/bungee/protocol/channel/ChannelAcceptor.java
new file mode 100644
index 00000000..2d432a57
--- /dev/null
+++ b/protocol/src/main/java/net/md_5/bungee/protocol/channel/ChannelAcceptor.java
@@ -0,0 +1,16 @@
+package net.md_5.bungee.protocol.channel;
+
+import io.netty.channel.Channel;
+
+@FunctionalInterface
+public interface ChannelAcceptor
+{
+
+    /**
+     * Inside this method the pipeline should be initialized.
+     *
+     * @param channel the channel to be accepted and initialized
+     * @return if the channel was accepted
+     */
+    boolean accept(Channel channel);
+}
diff --git a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java
index 305718e7..c39239bc 100644
--- a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java
+++ b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java
@@ -95,6 +95,7 @@ import net.md_5.bungee.module.ModuleManager;
 import net.md_5.bungee.netty.PipelineUtils;
 import net.md_5.bungee.protocol.DefinedPacket;
 import net.md_5.bungee.protocol.ProtocolConstants;
+import net.md_5.bungee.protocol.channel.BungeeChannelInitializer;
 import net.md_5.bungee.protocol.packet.PluginMessage;
 import net.md_5.bungee.query.RemoteQuery;
 import net.md_5.bungee.scheduler.BungeeScheduler;
@@ -188,6 +189,21 @@ public class BungeeCord extends ProxyServer
         return (BungeeCord) ProxyServer.getInstance();
     }
 
+    private final Unsafe unsafe = new Unsafe()
+    {
+        @Getter
+        @Setter
+        private BungeeChannelInitializer frontendChannelInitializer;
+
+        @Getter
+        @Setter
+        private BungeeChannelInitializer backendChannelInitializer;
+
+        @Getter
+        @Setter
+        private BungeeChannelInitializer serverInfoChannelInitializer;
+    };
+
     @SuppressFBWarnings("DM_DEFAULT_ENCODING")
     public BungeeCord() throws IOException
     {
@@ -360,7 +376,7 @@ public class BungeeCord extends ProxyServer
                     .channel( PipelineUtils.getServerChannel( info.getSocketAddress() ) )
                     .option( ChannelOption.SO_REUSEADDR, true ) // TODO: Move this elsewhere!
                     .childAttr( PipelineUtils.LISTENER, info )
-                    .childHandler( PipelineUtils.SERVER_CHILD )
+                    .childHandler( unsafe().getFrontendChannelInitializer().getChannelInitializer() )
                     .group( eventLoops )
                     .localAddress( info.getSocketAddress() )
                     .bind().addListener( listener );
@@ -831,4 +847,10 @@ public class BungeeCord extends ProxyServer
     {
         return new BungeeTitle();
     }
+
+    @Override
+    public Unsafe unsafe()
+    {
+        return unsafe;
+    }
 }
diff --git a/proxy/src/main/java/net/md_5/bungee/BungeeServerInfo.java b/proxy/src/main/java/net/md_5/bungee/BungeeServerInfo.java
index 671cf96f..7700f1d6 100644
--- a/proxy/src/main/java/net/md_5/bungee/BungeeServerInfo.java
+++ b/proxy/src/main/java/net/md_5/bungee/BungeeServerInfo.java
@@ -186,7 +186,7 @@ public class BungeeServerInfo implements ServerInfo
         new Bootstrap()
                 .channel( PipelineUtils.getChannel( socketAddress ) )
                 .group( BungeeCord.getInstance().eventLoops )
-                .handler( PipelineUtils.BASE_SERVERSIDE )
+                .handler( ProxyServer.getInstance().unsafe().getServerInfoChannelInitializer().getChannelInitializer() )
                 .option( ChannelOption.CONNECT_TIMEOUT_MILLIS, BungeeCord.getInstance().getConfig().getRemotePingTimeout() )
                 .remoteAddress( socketAddress )
                 .connect()
diff --git a/proxy/src/main/java/net/md_5/bungee/ServerConnector.java b/proxy/src/main/java/net/md_5/bungee/ServerConnector.java
index b58a01c6..8f52c50e 100644
--- a/proxy/src/main/java/net/md_5/bungee/ServerConnector.java
+++ b/proxy/src/main/java/net/md_5/bungee/ServerConnector.java
@@ -104,6 +104,7 @@ public class ServerConnector extends PacketHandler
     @Override
     public void connected(ChannelWrapper channel) throws Exception
     {
+        channel.setVersion( user.getPendingConnection().getVersion() );
         this.ch = channel;
 
         this.handshakeHandler = new ForgeServerHandler( user, ch, target );
diff --git a/proxy/src/main/java/net/md_5/bungee/UserConnection.java b/proxy/src/main/java/net/md_5/bungee/UserConnection.java
index 8f121aa9..6d661494 100644
--- a/proxy/src/main/java/net/md_5/bungee/UserConnection.java
+++ b/proxy/src/main/java/net/md_5/bungee/UserConnection.java
@@ -3,10 +3,8 @@ package net.md_5.bungee;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 import io.netty.bootstrap.Bootstrap;
-import io.netty.channel.Channel;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
-import io.netty.channel.ChannelInitializer;
 import io.netty.channel.ChannelOption;
 import io.netty.util.internal.PlatformDependent;
 import java.net.InetSocketAddress;
@@ -50,8 +48,6 @@ import net.md_5.bungee.netty.ChannelWrapper;
 import net.md_5.bungee.netty.HandlerBoss;
 import net.md_5.bungee.netty.PipelineUtils;
 import net.md_5.bungee.protocol.DefinedPacket;
-import net.md_5.bungee.protocol.MinecraftDecoder;
-import net.md_5.bungee.protocol.MinecraftEncoder;
 import net.md_5.bungee.protocol.PacketWrapper;
 import net.md_5.bungee.protocol.Protocol;
 import net.md_5.bungee.protocol.ProtocolConstants;
@@ -362,17 +358,6 @@ public final class UserConnection implements ProxiedPlayer
 
         pendingConnects.add( target );
 
-        ChannelInitializer initializer = new ChannelInitializer()
-        {
-            @Override
-            protected void initChannel(Channel ch) throws Exception
-            {
-                PipelineUtils.BASE_SERVERSIDE.initChannel( ch );
-                ch.pipeline().addAfter( PipelineUtils.FRAME_DECODER, PipelineUtils.PACKET_DECODER, new MinecraftDecoder( Protocol.HANDSHAKE, false, getPendingConnection().getVersion() ) );
-                ch.pipeline().addAfter( PipelineUtils.FRAME_PREPENDER, PipelineUtils.PACKET_ENCODER, new MinecraftEncoder( Protocol.HANDSHAKE, false, getPendingConnection().getVersion() ) );
-                ch.pipeline().get( HandlerBoss.class ).setHandler( new ServerConnector( bungee, UserConnection.this, target ) );
-            }
-        };
         ChannelFutureListener listener = new ChannelFutureListener()
         {
             @Override
@@ -401,13 +386,16 @@ public final class UserConnection implements ProxiedPlayer
                     {
                         sendMessage( bungee.getTranslation( "fallback_kick", connectionFailMessage( future.cause() ) ) );
                     }
+                } else
+                {
+                    future.channel().pipeline().get( HandlerBoss.class ).setHandler( new ServerConnector( bungee, UserConnection.this, target ) );
                 }
             }
         };
         Bootstrap b = new Bootstrap()
                 .channel( PipelineUtils.getChannel( target.getAddress() ) )
                 .group( ch.getHandle().eventLoop() )
-                .handler( initializer )
+                .handler( bungee.unsafe().getBackendChannelInitializer().getChannelInitializer() )
                 .option( ChannelOption.CONNECT_TIMEOUT_MILLIS, request.getConnectTimeout() )
                 .remoteAddress( target.getAddress() );
         // Windows is bugged, multi homed users will just have to live with random connecting IPs
diff --git a/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java b/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java
index 2446f489..8fcf0ce7 100644
--- a/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java
+++ b/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java
@@ -4,7 +4,6 @@ import com.google.common.base.Preconditions;
 import io.netty.buffer.PooledByteBufAllocator;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelException;
-import io.netty.channel.ChannelInitializer;
 import io.netty.channel.ChannelOption;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.ServerChannel;
@@ -51,33 +50,33 @@ import net.md_5.bungee.protocol.Protocol;
 import net.md_5.bungee.protocol.Varint21FrameDecoder;
 import net.md_5.bungee.protocol.Varint21LengthFieldExtraBufPrepender;
 import net.md_5.bungee.protocol.Varint21LengthFieldPrepender;
+import net.md_5.bungee.protocol.channel.BungeeChannelInitializer;
+import net.md_5.bungee.protocol.channel.ChannelAcceptor;
 
 public class PipelineUtils
 {
 
     public static final AttributeKey<ListenerInfo> LISTENER = AttributeKey.valueOf( "ListerInfo" );
-    public static final ChannelInitializer<Channel> SERVER_CHILD = new ChannelInitializer<Channel>()
+
+    private static void setChannelInitializerHolders()
     {
-        @Override
-        protected void initChannel(Channel ch) throws Exception
+        ProxyServer.getInstance().unsafe().setFrontendChannelInitializer( BungeeChannelInitializer.create( ch ->
         {
             SocketAddress remoteAddress = ( ch.remoteAddress() == null ) ? ch.parent().localAddress() : ch.remoteAddress();
 
             if ( BungeeCord.getInstance().getConnectionThrottle() != null && BungeeCord.getInstance().getConnectionThrottle().throttle( remoteAddress ) )
             {
-                ch.close();
-                return;
+                return false;
             }
 
             ListenerInfo listener = ch.attr( LISTENER ).get();
 
             if ( BungeeCord.getInstance().getPluginManager().callEvent( new ClientConnectEvent( remoteAddress, listener ) ).isCancelled() )
             {
-                ch.close();
-                return;
+                return false;
             }
 
-            BASE.initChannel( ch );
+            BASE.accept( ch );
             ch.pipeline().addBefore( FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder() );
             ch.pipeline().addAfter( FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder( Protocol.HANDSHAKE, true, ProxyServer.getInstance().getProtocolVersion() ) );
             ch.pipeline().addAfter( FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder( Protocol.HANDSHAKE, true, ProxyServer.getInstance().getProtocolVersion() ) );
@@ -88,10 +87,24 @@ public class PipelineUtils
             {
                 ch.pipeline().addFirst( new HAProxyMessageDecoder() );
             }
-        }
-    };
-    public static final Base BASE = new Base( false );
-    public static final Base BASE_SERVERSIDE = new Base( true );
+
+            return true;
+        } ) );
+
+        ProxyServer.getInstance().unsafe().setBackendChannelInitializer( BungeeChannelInitializer.create( ch ->
+        {
+            PipelineUtils.BASE_SERVERSIDE.accept( ch );
+            ch.pipeline().addAfter( PipelineUtils.FRAME_DECODER, PipelineUtils.PACKET_DECODER, new MinecraftDecoder( Protocol.HANDSHAKE, false, ProxyServer.getInstance().getProtocolVersion() ) );
+            ch.pipeline().addAfter( PipelineUtils.FRAME_PREPENDER, PipelineUtils.PACKET_ENCODER, new MinecraftEncoder( Protocol.HANDSHAKE, false, ProxyServer.getInstance().getProtocolVersion() ) );
+
+            return true;
+        } ) );
+
+        ProxyServer.getInstance().unsafe().setServerInfoChannelInitializer( BungeeChannelInitializer.create( BASE_SERVERSIDE ) );
+    }
+
+    private static final ChannelAcceptor BASE = new Base( false );
+    private static final ChannelAcceptor BASE_SERVERSIDE = new Base( true );
     private static final KickStringWriter legacyKicker = new KickStringWriter();
     private static final Varint21LengthFieldExtraBufPrepender serverFramePrepender = new Varint21LengthFieldExtraBufPrepender();
     public static final String TIMEOUT_HANDLER = "timeout";
@@ -137,6 +150,8 @@ public class PipelineUtils
                 }
             }
         }
+
+        setChannelInitializerHolders();
     }
 
     public static EventLoopGroup newEventLoopGroup(int threads, ThreadFactory factory)
@@ -179,13 +194,13 @@ public class PipelineUtils
 
     @NoArgsConstructor // for backwards compatibility
     @AllArgsConstructor
-    public static final class Base extends ChannelInitializer<Channel>
+    public static final class Base implements ChannelAcceptor
     {
 
         private boolean toServer = false;
 
         @Override
-        public void initChannel(Channel ch) throws Exception
+        public boolean accept(Channel ch)
         {
             try
             {
@@ -204,6 +219,8 @@ public class PipelineUtils
             ch.pipeline().addLast( FRAME_PREPENDER, ( toServer ) ? serverFramePrepender : new Varint21LengthFieldPrepender() );
 
             ch.pipeline().addLast( BOSS_HANDLER, new HandlerBoss() );
+
+            return true;
         }
     }
 }