diff --git a/src/main/java/io/github/gaming32/worldhost/ProxyClient.java b/src/main/java/io/github/gaming32/worldhost/ProxyClient.java deleted file mode 100644 index d95d10d..0000000 --- a/src/main/java/io/github/gaming32/worldhost/ProxyClient.java +++ /dev/null @@ -1,78 +0,0 @@ -package io.github.gaming32.worldhost; - -import io.github.gaming32.worldhost.protocol.proxy.ProxyPassthrough; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.Socket; -import java.util.Arrays; -import java.util.function.Supplier; - -public final class ProxyClient { - private final Thread thread; - private final Socket socket; - private final InetAddress remoteAddress; - private final long connectionId; - private final Supplier proxy; - - private boolean closed; - - public ProxyClient( - int port, - InetAddress remoteAddress, - long connectionId, - Supplier proxy - ) throws IOException { - thread = Thread.ofVirtual().name("ProxyClient for " + connectionId).unstarted(this::run); - socket = new Socket(InetAddress.getLoopbackAddress(), port); - this.remoteAddress = remoteAddress; - this.connectionId = connectionId; - this.proxy = proxy; - if (proxy.get() == null) { - WorldHost.LOGGER.error("ProxyPassthrough for {} ({}) is initially null.", connectionId, remoteAddress); - } - } - - private void run() { - WorldHost.LOGGER.info("Starting proxy client from {}", remoteAddress); - try { - final var is = socket.getInputStream(); - final byte[] b = new byte[0xffff]; - int n; - while ((n = is.read(b)) != -1) { - final ProxyPassthrough proxy = this.proxy.get(); - if (proxy == null) break; - if (n == 0) continue; - proxy.proxyS2CPacket(connectionId, Arrays.copyOf(b, n)); - } - } catch (IOException e) { - WorldHost.LOGGER.error("Proxy client connection for {} has error", remoteAddress, e); - } - WorldHost.CONNECTED_PROXY_CLIENTS.remove(connectionId); - close(); - final ProxyPassthrough proxy = this.proxy.get(); - if (proxy != null) { - proxy.proxyDisconnect(connectionId); - } - WorldHost.LOGGER.info("Proxy client connection for {} closed", remoteAddress); - } - - public void start() { - thread.start(); - } - - public void close() { - if (closed) return; - closed = true; - try { - socket.close(); - } catch (IOException e) { - WorldHost.LOGGER.error("Failed to close proxy client socket for {}", remoteAddress, e); - } - } - - public OutputStream getOutputStream() throws IOException { - return socket.getOutputStream(); - } -} diff --git a/src/main/java/io/github/gaming32/worldhost/WorldHost.java b/src/main/java/io/github/gaming32/worldhost/WorldHost.java index 66584bd..5755d41 100644 --- a/src/main/java/io/github/gaming32/worldhost/WorldHost.java +++ b/src/main/java/io/github/gaming32/worldhost/WorldHost.java @@ -11,11 +11,14 @@ import io.github.gaming32.worldhost.protocol.ProtocolClient; import io.github.gaming32.worldhost.protocol.proxy.ProxyPassthrough; import io.github.gaming32.worldhost.protocol.proxy.ProxyProtocolClient; +import io.github.gaming32.worldhost.proxy.ProxyClient; import io.github.gaming32.worldhost.toast.WHToast; import io.github.gaming32.worldhost.upnp.Gateway; import io.github.gaming32.worldhost.upnp.GatewayFinder; import io.github.gaming32.worldhost.versions.Components; import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap; @@ -36,6 +39,7 @@ import net.minecraft.network.protocol.status.ClientboundStatusResponsePacket; import net.minecraft.network.protocol.status.ServerStatus; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.network.ServerConnectionListener; import net.minecraft.server.players.GameProfileCache; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.methods.HttpGet; @@ -52,7 +56,9 @@ import java.io.InputStreamReader; import java.io.IOException; import java.io.UncheckedIOException; +import java.lang.reflect.Constructor; import java.net.InetAddress; +import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -223,6 +229,9 @@ public class WorldHost public static boolean shareWorldOnLoad; + public static SocketAddress proxySocketAddress; + public static Constructor> channelInitializerConstructor; + //#if FABRIC @Override public void onInitializeClient() { @@ -726,7 +735,7 @@ public static void proxyConnect(long connectionId, InetAddress remoteAddr, Suppl return; } try { - final ProxyClient proxyClient = new ProxyClient(server.getPort(), remoteAddr, connectionId, proxy); + final ProxyClient proxyClient = new ProxyClient(remoteAddr, connectionId, proxy); WorldHost.CONNECTED_PROXY_CLIENTS.put(connectionId, proxyClient); proxyClient.start(); } catch (IOException e) { @@ -734,15 +743,10 @@ public static void proxyConnect(long connectionId, InetAddress remoteAddr, Suppl } } - // TODO: Implement using a proper Netty channel to introduce packets directly to the Netty pipeline somehow. public static void proxyPacket(long connectionId, byte[] data) { final ProxyClient proxyClient = WorldHost.CONNECTED_PROXY_CLIENTS.get(connectionId); if (proxyClient != null) { - try { - proxyClient.getOutputStream().write(data); - } catch (IOException e) { - WorldHost.LOGGER.error("Failed to write to ProxyClient", e); - } + proxyClient.send(data); } else { WorldHost.LOGGER.warn("Received packet for unknown connection {}", connectionId); } @@ -866,6 +870,15 @@ private static Path getGameDir() { //#endif } + public static ChannelInitializer createChannelInitializer(ServerConnectionListener listener) { + try { + return channelInitializerConstructor.newInstance(listener); + } catch (ReflectiveOperationException e) { + // TODO: UncheckedReflectiveOperationException when 1.20.4+ becomes the minimum + throw new RuntimeException(e); + } + } + //#if FORGELIKE //#if MC >= 1.20.5 //$$ @EventBusSubscriber(modid = MOD_ID, bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) diff --git a/src/main/java/io/github/gaming32/worldhost/mixin/MixinIntegratedServer.java b/src/main/java/io/github/gaming32/worldhost/mixin/MixinIntegratedServer.java index 77c5107..50b4e37 100644 --- a/src/main/java/io/github/gaming32/worldhost/mixin/MixinIntegratedServer.java +++ b/src/main/java/io/github/gaming32/worldhost/mixin/MixinIntegratedServer.java @@ -1,7 +1,8 @@ package io.github.gaming32.worldhost.mixin; import com.mojang.datafixers.DataFixer; -import io.github.gaming32.worldhost.ProxyClient; +import io.github.gaming32.worldhost.proxy.ProxyChannels; +import io.github.gaming32.worldhost.proxy.ProxyClient; import io.github.gaming32.worldhost.WorldHost; import io.github.gaming32.worldhost.versions.Components; import net.minecraft.ChatFormatting; @@ -148,4 +149,16 @@ private void shareWorldOnLoad(UUID uuid, CallbackInfo ci) { Components.copyOnClickText(externalIp), port ); } + + @Inject( + method = "publishServer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/network/ServerConnectionListener;startTcpServerListener(Ljava/net/InetAddress;I)V", + shift = At.Shift.AFTER + ) + ) + private void startProxyChannel(GameType gameMode, boolean cheats, int port, CallbackInfoReturnable cir) { + WorldHost.proxySocketAddress = ProxyChannels.startProxyChannel(getConnection()); + } } diff --git a/src/main/java/io/github/gaming32/worldhost/mixin/MixinServerConnectionListener_1.java b/src/main/java/io/github/gaming32/worldhost/mixin/MixinServerConnectionListener_1.java new file mode 100644 index 0000000..ba5b932 --- /dev/null +++ b/src/main/java/io/github/gaming32/worldhost/mixin/MixinServerConnectionListener_1.java @@ -0,0 +1,20 @@ +package io.github.gaming32.worldhost.mixin; + +import io.github.gaming32.worldhost.WorldHost; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import net.minecraft.server.network.ServerConnectionListener; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(targets = "net.minecraft.server.network.ServerConnectionListener$1") +public abstract class MixinServerConnectionListener_1 extends ChannelInitializer { + @Inject(method = "", at = @At("TAIL")) + private void storeClass(ServerConnectionListener this$0, CallbackInfo ci) throws NoSuchMethodException { + if (WorldHost.channelInitializerConstructor == null) { + WorldHost.channelInitializerConstructor = getClass().getDeclaredConstructor(ServerConnectionListener.class); + } + } +} diff --git a/src/main/java/io/github/gaming32/worldhost/mixin/ServerConnectionListenerAccessor.java b/src/main/java/io/github/gaming32/worldhost/mixin/ServerConnectionListenerAccessor.java new file mode 100644 index 0000000..4e2ab1b --- /dev/null +++ b/src/main/java/io/github/gaming32/worldhost/mixin/ServerConnectionListenerAccessor.java @@ -0,0 +1,14 @@ +package io.github.gaming32.worldhost.mixin; + +import io.netty.channel.ChannelFuture; +import net.minecraft.server.network.ServerConnectionListener; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.List; + +@Mixin(ServerConnectionListener.class) +public interface ServerConnectionListenerAccessor { + @Accessor + List getChannels(); +} diff --git a/src/main/java/io/github/gaming32/worldhost/proxy/ProxyChannels.java b/src/main/java/io/github/gaming32/worldhost/proxy/ProxyChannels.java new file mode 100644 index 0000000..dad266a --- /dev/null +++ b/src/main/java/io/github/gaming32/worldhost/proxy/ProxyChannels.java @@ -0,0 +1,29 @@ +package io.github.gaming32.worldhost.proxy; + +import io.github.gaming32.worldhost.WorldHost; +import io.github.gaming32.worldhost.mixin.ServerConnectionListenerAccessor; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalServerChannel; +import net.minecraft.server.network.ServerConnectionListener; + +import java.net.SocketAddress; + +public class ProxyChannels { + public static SocketAddress startProxyChannel(ServerConnectionListener listener) { + final ServerConnectionListenerAccessor accessor = (ServerConnectionListenerAccessor)listener; + ChannelFuture channel; + synchronized (accessor.getChannels()) { + channel = new ServerBootstrap() + .channel(LocalServerChannel.class) + .childHandler(WorldHost.createChannelInitializer(listener)) + .group(ServerConnectionListener.SERVER_EVENT_GROUP.get()) + .localAddress(LocalAddress.ANY) + .bind() + .syncUninterruptibly(); + accessor.getChannels().add(channel); + } + return channel.channel().localAddress(); + } +} diff --git a/src/main/java/io/github/gaming32/worldhost/proxy/ProxyClient.java b/src/main/java/io/github/gaming32/worldhost/proxy/ProxyClient.java new file mode 100644 index 0000000..0b6e74d --- /dev/null +++ b/src/main/java/io/github/gaming32/worldhost/proxy/ProxyClient.java @@ -0,0 +1,122 @@ +package io.github.gaming32.worldhost.proxy; + +import io.github.gaming32.worldhost.WorldHost; +import io.github.gaming32.worldhost.protocol.proxy.ProxyPassthrough; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.local.LocalChannel; +import net.minecraft.server.network.ServerConnectionListener; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.function.Supplier; + +public final class ProxyClient extends SimpleChannelInboundHandler { + private static final int PACKET_SIZE = 0xffff; + + private final InetAddress remoteAddress; + private final long connectionId; + private final Supplier proxy; + + private Channel channel; + private boolean closed; + + public ProxyClient( + InetAddress remoteAddress, + long connectionId, + Supplier proxy + ) throws IOException { + this.remoteAddress = remoteAddress; + this.connectionId = connectionId; + this.proxy = proxy; + if (proxy.get() == null) { + WorldHost.LOGGER.error("ProxyPassthrough for {} ({}) is initially null.", connectionId, remoteAddress); + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + channel = ctx.channel(); + WorldHost.LOGGER.info("Started proxy client from {}", remoteAddress); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + WorldHost.CONNECTED_PROXY_CLIENTS.remove(connectionId); + close(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + WorldHost.LOGGER.error("Proxy client connection for {} had error", remoteAddress, cause); + WorldHost.CONNECTED_PROXY_CLIENTS.remove(connectionId); + close(); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { + final ProxyPassthrough proxy = this.proxy.get(); + if (proxy == null) { + close(); + return; + } + + while (true) { + int len = Math.min(msg.readableBytes(), PACKET_SIZE); + if (len == 0) break; + final byte[] buffer = new byte[len]; + msg.readBytes(buffer); + proxy.proxyS2CPacket(connectionId, buffer); + } + } + + public void start() { + WorldHost.LOGGER.info("Starting proxy client from {}", remoteAddress); + new Bootstrap() + .group(ServerConnectionListener.SERVER_EVENT_GROUP.get()) + .handler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast("handler", ProxyClient.this); + } + }) + .channel(LocalChannel.class) + .connect(WorldHost.proxySocketAddress) + .syncUninterruptibly(); + } + + public void close() { + if (closed) return; + closed = true; + try { + channel.close(); + final ProxyPassthrough proxy = this.proxy.get(); + if (proxy != null) { + proxy.proxyDisconnect(connectionId); + } + WorldHost.LOGGER.info("Proxy client connection for {} closed", remoteAddress); + } catch (Exception e) { + WorldHost.LOGGER.error("Proxy client connection for {} failed to close", remoteAddress, e); + } + } + + public void send(byte[] message) { + if (channel.eventLoop().inEventLoop()) { + doSend(message); + } else { + channel.eventLoop().execute(() -> doSend(message)); + } + } + + private void doSend(byte[] message) { + channel.writeAndFlush(Unpooled.wrappedBuffer(message)) + .addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + } +} diff --git a/src/main/resources/world-host.mixins.json b/src/main/resources/world-host.mixins.json index f15f10c..6bc25e5 100644 --- a/src/main/resources/world-host.mixins.json +++ b/src/main/resources/world-host.mixins.json @@ -6,7 +6,9 @@ "mixins": [ "MixinCommands", "MixinLevelSummary", - "MixinPublishCommand" + "MixinPublishCommand", + "MixinServerConnectionListener_1", + "ServerConnectionListenerAccessor" ], "client": [ "MinecraftAccessor",