diff --git a/pom.xml b/pom.xml index 34672b6f3..2261f9583 100644 --- a/pom.xml +++ b/pom.xml @@ -119,5 +119,11 @@ jcl-core 2.8 + + io.netty + netty-all + 4.1.52.Final + + \ No newline at end of file diff --git a/src/main/java/de/bixilon/minosoft/modding/MinosoftMod.java b/src/main/java/de/bixilon/minosoft/modding/MinosoftMod.java index c74fae74f..5328049d8 100644 --- a/src/main/java/de/bixilon/minosoft/modding/MinosoftMod.java +++ b/src/main/java/de/bixilon/minosoft/modding/MinosoftMod.java @@ -17,15 +17,7 @@ import de.bixilon.minosoft.modding.event.EventManager; import de.bixilon.minosoft.modding.loading.ModInfo; import de.bixilon.minosoft.modding.loading.ModPhases; -interface MinosoftModInterface { - /** - * @param phase The current loading phase - * @return If the loading was successful. If not, the mod is getting disabled. - */ - boolean start(ModPhases phase); -} - -public abstract class MinosoftMod implements MinosoftModInterface { +public abstract class MinosoftMod { private final EventManager eventManager = new EventManager(); protected boolean enabled = true; private ModInfo info; @@ -58,4 +50,10 @@ public abstract class MinosoftMod implements MinosoftModInterface { public Logger getLogger() { return logger; } + + /** + * @param phase The current loading phase + * @return If the loading was successful. If not, the mod is getting disabled. + */ + public abstract boolean start(ModPhases phase); } \ No newline at end of file diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/Connection.java b/src/main/java/de/bixilon/minosoft/protocol/network/Connection.java index ef026e28e..0e8955444 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/Connection.java +++ b/src/main/java/de/bixilon/minosoft/protocol/network/Connection.java @@ -25,7 +25,6 @@ import de.bixilon.minosoft.game.datatypes.objectLoader.versions.Versions; import de.bixilon.minosoft.gui.main.ConnectionChangeCallback; import de.bixilon.minosoft.logging.Log; import de.bixilon.minosoft.logging.LogLevels; -import de.bixilon.minosoft.modding.event.EventListener; import de.bixilon.minosoft.modding.event.EventManager; import de.bixilon.minosoft.ping.ServerListPing; import de.bixilon.minosoft.protocol.modding.channels.DefaultPluginChannels; @@ -50,7 +49,7 @@ import java.util.concurrent.LinkedBlockingQueue; public class Connection { public static int lastConnectionId; - final Network network = new Network(this); + final Network network = Network.getNetworkInstance(this); final PacketHandler handler = new PacketHandler(this); final PacketSender sender = new PacketSender(this); final LinkedBlockingQueue handlingQueue = new LinkedBlockingQueue<>(); @@ -73,6 +72,7 @@ public class Connection { ConnectionReasons nextReason; ConnectionPing connectionStatusPing; ServerListPing lastPing; + Exception lastException; public Connection(int connectionId, String hostname, Player player) { this.connectionId = connectionId; @@ -81,7 +81,7 @@ public class Connection { } public void resolve(ConnectionReasons reason, int protocolId) { - network.lastException = null; + lastException = null; this.desiredVersionNumber = protocolId; Thread resolveThread = new Thread(() -> { @@ -93,7 +93,7 @@ public class Connection { addresses = DNSUtil.getServerAddresses(hostname); } catch (TextParseException e) { setConnectionState(ConnectionStates.FAILED_NO_RETRY); - network.lastException = e; + lastException = e; e.printStackTrace(); return; } @@ -232,7 +232,7 @@ public class Connection { e.printStackTrace(); } Log.fatal(String.format("Could not load mapping for %s. This version seems to be unsupported!", version)); - network.lastException = new RuntimeException(String.format("Mappings could not be loaded: %s", e.getLocalizedMessage())); + lastException = new RuntimeException(String.format("Mappings could not be loaded: %s", e.getLocalizedMessage())); setConnectionState(ConnectionStates.FAILED_NO_RETRY); } } @@ -396,7 +396,7 @@ public class Connection { } public Exception getLastConnectionException() { - return network.lastException; + return (lastException != null) ? lastException : network.getLastException(); } public void addConnectionChangeCallback(ConnectionChangeCallback callback) { @@ -422,8 +422,4 @@ public class Connection { public void unregisterEvents(EventManager... eventManagers) { this.eventManagers.removeAll(Arrays.asList(eventManagers)); } - - public HashSet getAllEvents() { - return eventManagers; - } } diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/Network.java b/src/main/java/de/bixilon/minosoft/protocol/network/Network.java index 7d16ce8bd..d978071d5 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/Network.java +++ b/src/main/java/de/bixilon/minosoft/protocol/network/Network.java @@ -13,270 +13,20 @@ package de.bixilon.minosoft.protocol.network; -import de.bixilon.minosoft.logging.Log; -import de.bixilon.minosoft.logging.LogLevels; -import de.bixilon.minosoft.protocol.packets.ClientboundPacket; +import de.bixilon.minosoft.protocol.network.socket.SocketNetwork; import de.bixilon.minosoft.protocol.packets.ServerboundPacket; -import de.bixilon.minosoft.protocol.packets.clientbound.interfaces.PacketCompressionInterface; -import de.bixilon.minosoft.protocol.packets.clientbound.login.PacketEncryptionRequest; -import de.bixilon.minosoft.protocol.packets.clientbound.login.PacketLoginSuccess; -import de.bixilon.minosoft.protocol.packets.serverbound.login.PacketEncryptionResponse; -import de.bixilon.minosoft.protocol.protocol.*; import de.bixilon.minosoft.util.ServerAddress; -import de.bixilon.minosoft.util.Util; -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.SecretKey; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.InvocationTargetException; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketException; -import java.util.concurrent.LinkedBlockingQueue; - -public class Network { - final Connection connection; - final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); - Thread socketRThread; - Thread socketSThread; - int compressionThreshold = -1; - Socket socket; - OutputStream outputStream; - InputStream inputStream; - boolean encryptionEnabled = false; - SecretKey secretKey; - Exception lastException; - - public Network(Connection connection) { - this.connection = connection; +public interface Network { + static Network getNetworkInstance(Connection connection) { + return new SocketNetwork(connection); } - public void connect(ServerAddress address) { - // check if we are already connected or try to connect - if (connection.isConnected() || connection.getConnectionState() == ConnectionStates.CONNECTING) { - return; - } - // wait for data or send until it should disconnect - // first send, then receive - // something to send it, send it - // send, flush and remove - // everything sent for now, waiting for data - // add to queue - // Could not connect - socketRThread = new Thread(() -> { - try { - socket = new Socket(); - socket.setSoTimeout(ProtocolDefinition.SOCKET_CONNECT_TIMEOUT); - socket.connect(new InetSocketAddress(address.getHostname(), address.getPort()), ProtocolDefinition.SOCKET_CONNECT_TIMEOUT); - // connected, use minecraft timeout - socket.setSoTimeout(ProtocolDefinition.SOCKET_TIMEOUT); - connection.setConnectionState(ConnectionStates.HANDSHAKING); - socket.setKeepAlive(true); - outputStream = socket.getOutputStream(); - inputStream = socket.getInputStream(); + void connect(ServerAddress address); - socketRThread.setName(String.format("%d/SocketR", connection.getConnectionId())); + void sendPacket(ServerboundPacket packet); - socketSThread = new Thread(() -> { - try { - while (connection.getConnectionState() != ConnectionStates.DISCONNECTING) { - // wait for data or send until it should disconnect + void disconnect(); - // check if still connected - if (!socket.isConnected() || socket.isClosed()) { - break; - } - - ServerboundPacket packet = queue.take(); - packet.log(); - queue.remove(packet); - byte[] data = packet.write(connection).getOutBytes(); - if (compressionThreshold >= 0) { - // compression is enabled - // check if there is a need to compress it and if so, do it! - OutByteBuffer outRawBuffer = new OutByteBuffer(connection); - if (data.length >= compressionThreshold) { - // compress it - OutByteBuffer compressedBuffer = new OutByteBuffer(connection); - byte[] compressed = Util.compress(data); - compressedBuffer.writeVarInt(data.length); - compressedBuffer.writeBytes(compressed); - outRawBuffer.writeVarInt(compressedBuffer.getOutBytes().length); - outRawBuffer.writeBytes(compressedBuffer.getOutBytes()); - } else { - outRawBuffer.writeVarInt(data.length + 1); // 1 for the compressed length (0) - outRawBuffer.writeVarInt(0); - outRawBuffer.writeBytes(data); - } - data = outRawBuffer.getOutBytes(); - } else { - // append packet length - OutByteBuffer bufferWithLengthPrefix = new OutByteBuffer(connection); - bufferWithLengthPrefix.writeVarInt(data.length); - bufferWithLengthPrefix.writeBytes(data); - data = bufferWithLengthPrefix.getOutBytes(); - } - - outputStream.write(data); - outputStream.flush(); - if (packet instanceof PacketEncryptionResponse) { - // enable encryption - secretKey = ((PacketEncryptionResponse) packet).getSecretKey(); - enableEncryption(secretKey); - // wake up other thread - socketRThread.interrupt(); - } - } - } catch (IOException | InterruptedException ignored) { - } - }); - socketSThread.setName(String.format("%d/SocketS", connection.getConnectionId())); - socketSThread.start(); - - while (connection.getConnectionState() != ConnectionStates.DISCONNECTING) { - // wait for data or send until it should disconnect - // first send, then receive - - // check if still connected - if (!socket.isConnected() || socket.isClosed()) { - break; - } - -// everything sent for now, waiting for data - int numRead = 0; - int length = 0; - int read; - do { - read = inputStream.read(); - if (read == -1) { - disconnect(); - return; - } - int value = (read & 0b01111111); - length |= (value << (7 * numRead)); - - numRead++; - if (numRead > 5) { - throw new RuntimeException("VarInt is too big"); - } - } while ((read & 0b10000000) != 0); - if (length > ProtocolDefinition.PROTOCOL_PACKET_MAX_SIZE) { - Log.protocol(String.format("Server sent us a to big packet (%d bytes > %d bytes)", length, ProtocolDefinition.PROTOCOL_PACKET_MAX_SIZE)); - inputStream.skip(length); - continue; - } - byte[] data = inputStream.readNBytes(length); - - if (compressionThreshold >= 0) { - // compression is enabled - // check if there is a need to decompress it and if so, do it! - InByteBuffer rawBuffer = new InByteBuffer(data, connection); - int packetSize = rawBuffer.readVarInt(); - byte[] left = rawBuffer.readBytesLeft(); - if (packetSize == 0) { - // no need - data = left; - } else { - // need to decompress data - data = Util.decompress(left, connection).readBytesLeft(); - } - } - - InPacketBuffer inPacketBuffer = new InPacketBuffer(data, connection); - Packets.Clientbound packet = null; - try { - packet = connection.getPacketByCommand(connection.getConnectionState(), inPacketBuffer.getCommand()); - if (packet == null) { - Log.fatal(String.format("Version packet enum does not contain a packet with id 0x%x. Your version.json is broken!", inPacketBuffer.getCommand())); - System.exit(1); - } - Class clazz = packet.getClazz(); - - if (clazz == null) { - Log.warn(String.format("[IN] Received unknown packet (id=0x%x, name=%s, length=%d, dataLength=%d, version=%s, state=%s)", inPacketBuffer.getCommand(), packet, inPacketBuffer.getLength(), inPacketBuffer.getBytesLeft(), connection.getVersion(), connection.getConnectionState())); - continue; - } - try { - ClientboundPacket packetInstance = clazz.getConstructor().newInstance(); - boolean success = packetInstance.read(inPacketBuffer); - if (inPacketBuffer.getBytesLeft() > 0 || !success) { - // warn not all data used - Log.warn(String.format("[IN] Could not parse packet %s (used=%d, available=%d, total=%d, success=%s)", packet, inPacketBuffer.getPosition(), inPacketBuffer.getBytesLeft(), inPacketBuffer.getLength(), success)); - continue; - } - - //set special settings to avoid miss timing issues - if (packetInstance instanceof PacketLoginSuccess) { - connection.setConnectionState(ConnectionStates.PLAY); - } else if (packetInstance instanceof PacketCompressionInterface) { - compressionThreshold = ((PacketCompressionInterface) packetInstance).getThreshold(); - } else if (packetInstance instanceof PacketEncryptionRequest) { - // wait until response is ready - connection.handle(packetInstance); - try { - Thread.sleep(Integer.MAX_VALUE); - } catch (InterruptedException ignored) { - } - continue; - } - connection.handle(packetInstance); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - // safety first, but will not occur - e.printStackTrace(); - } - } catch (Exception e) { - Log.protocol(String.format("An error occurred while parsing an packet (%s): %s", packet, e)); - if (Log.getLevel().ordinal() >= LogLevels.DEBUG.ordinal()) { - e.printStackTrace(); - } - } - } - disconnect(); - } catch (IOException e) { - // Could not connect - if (Log.getLevel().ordinal() >= LogLevels.DEBUG.ordinal()) { - e.printStackTrace(); - } - if (socketSThread != null) { - socketSThread.interrupt(); - } - if (e instanceof SocketException && e.getMessage().equals("Socket closed")) { - return; - } - lastException = e; - connection.setConnectionState(ConnectionStates.FAILED); - } - }); - socketRThread.setName(String.format("%d/Socket", connection.getConnectionId())); - socketRThread.start(); - } - - public void sendPacket(ServerboundPacket p) { - queue.add(p); - } - - public void enableEncryption(SecretKey secretKey) { - Cipher cipherEncrypt = CryptManager.createNetCipherInstance(Cipher.ENCRYPT_MODE, secretKey); - Cipher cipherDecrypt = CryptManager.createNetCipherInstance(Cipher.DECRYPT_MODE, secretKey); - inputStream = new CipherInputStream(inputStream, cipherDecrypt); - outputStream = new CipherOutputStream(outputStream, cipherEncrypt); - encryptionEnabled = true; - Log.debug("Encryption enabled!"); - } - - public void disconnect() { - connection.setConnectionState(ConnectionStates.DISCONNECTING); - try { - socket.close(); - } catch (IOException e) { - e.printStackTrace(); - } - socketRThread.interrupt(); - socketSThread.interrupt(); - connection.setConnectionState(ConnectionStates.DISCONNECTED); - } + Exception getLastException(); } diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/netty/NettyNetwork.java b/src/main/java/de/bixilon/minosoft/protocol/network/netty/NettyNetwork.java new file mode 100644 index 000000000..989009233 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/protocol/network/netty/NettyNetwork.java @@ -0,0 +1,101 @@ +/* + * Codename Minosoft + * Copyright (C) 2020 Moritz Zwerger + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.protocol.network.netty; + +import de.bixilon.minosoft.logging.Log; +import de.bixilon.minosoft.protocol.network.Connection; +import de.bixilon.minosoft.protocol.network.Network; +import de.bixilon.minosoft.protocol.packets.ServerboundPacket; +import de.bixilon.minosoft.protocol.protocol.ConnectionStates; +import de.bixilon.minosoft.util.ServerAddress; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; + +import javax.crypto.SecretKey; + +public class NettyNetwork implements Network { + final Connection connection; + NioSocketChannel nioSocketChannel; + int compressionThreshold = -1; + + public NettyNetwork(Connection connection) { + this.connection = connection; + } + + @Override + public void connect(ServerAddress address) { + EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); + + Bootstrap clientBootstrap = new Bootstrap(); + clientBootstrap.group(eventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new TCPClientChannelInitializer(connection, this)); + + + try { + ChannelFuture channelFuture = clientBootstrap.connect(address.getHostname(), address.getPort()).sync(); + if (channelFuture.isSuccess()) { + connection.setConnectionState(ConnectionStates.HANDSHAKING); + } + channelFuture.channel().closeFuture().sync(); + } catch (InterruptedException e) { + Log.info(String.format("connection failed: %s", e)); + connection.setConnectionState(ConnectionStates.FAILED); + } finally { + connection.setConnectionState(ConnectionStates.DISCONNECTED); + eventLoopGroup.shutdownGracefully(); + } + } + + @Override + public void sendPacket(ServerboundPacket packet) { + if (this.nioSocketChannel.eventLoop().inEventLoop()) { + this.nioSocketChannel.writeAndFlush(packet); + return; + } + this.nioSocketChannel.eventLoop().execute(() -> NettyNetwork.this.nioSocketChannel.writeAndFlush(packet)); + } + + @Override + public void disconnect() { + + } + + @Override + public Exception getLastException() { + return null; + } + + public int getCompressionThreshold() { + return compressionThreshold; + } + + public void setNioChannel(NioSocketChannel nioSocketChannel) { + this.nioSocketChannel = nioSocketChannel; + } + + public void enableEncryption(SecretKey key) { + /* + this.nioSocketChannel.pipeline().addBefore("decoder", "decrypt", new EncryptionHandler(key)); + this.nioSocketChannel.pipeline().addBefore("encoder", "encrypt", new DecryptionHandler(key)); + Log.debug("Encryption enabled!"); + */ + //ToDo + Log.fatal("Encryption is not implemented in netty yet!"); + disconnect(); + } +} diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/netty/PacketDecoder.java b/src/main/java/de/bixilon/minosoft/protocol/network/netty/PacketDecoder.java new file mode 100644 index 000000000..eb9754d44 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/protocol/network/netty/PacketDecoder.java @@ -0,0 +1,141 @@ +/* + * Codename Minosoft + * Copyright (C) 2020 Moritz Zwerger + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.protocol.network.netty; + + +import de.bixilon.minosoft.logging.Log; +import de.bixilon.minosoft.logging.LogLevels; +import de.bixilon.minosoft.protocol.network.Connection; +import de.bixilon.minosoft.protocol.packets.ClientboundPacket; +import de.bixilon.minosoft.protocol.packets.clientbound.interfaces.PacketCompressionInterface; +import de.bixilon.minosoft.protocol.packets.clientbound.login.PacketEncryptionRequest; +import de.bixilon.minosoft.protocol.packets.clientbound.login.PacketLoginSuccess; +import de.bixilon.minosoft.protocol.protocol.*; +import de.bixilon.minosoft.util.Util; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +public class PacketDecoder extends ByteToMessageDecoder { + final Connection connection; + final NettyNetwork nettyNetwork; + + public PacketDecoder(Connection connection, NettyNetwork nettyNetwork) { + this.connection = connection; + this.nettyNetwork = nettyNetwork; + } + + @Override + protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) { + byteBuf.markReaderIndex(); + int numRead = 0; + int length = 0; + byte read; + do { + if (!byteBuf.isReadable()) { + byteBuf.resetReaderIndex(); + return; + } + read = byteBuf.readByte(); + int value = (read & 0b01111111); + length |= (value << (7 * numRead)); + + numRead++; + if (numRead > 5) { + throw new RuntimeException("VarInt is too big"); + } + } while ((read & 0b10000000) != 0); + if (length > ProtocolDefinition.PROTOCOL_PACKET_MAX_SIZE) { + Log.protocol(String.format("Server sent us a to big packet (%d bytes > %d bytes)", length, ProtocolDefinition.PROTOCOL_PACKET_MAX_SIZE)); + byteBuf.skipBytes(length); + return; + } + if (byteBuf.readableBytes() < length) { + byteBuf.resetReaderIndex(); + return; + } + + byte[] data; + ByteBuf dataBuf = byteBuf.readBytes(length); + if (dataBuf.hasArray()) { + data = dataBuf.array(); + } else { + data = new byte[length]; + dataBuf.getBytes(0, data); + } + + if (nettyNetwork.getCompressionThreshold() >= 0) { + // compression is enabled + // check if there is a need to decompress it and if so, do it! + InByteBuffer rawBuffer = new InByteBuffer(data, connection); + int packetSize = rawBuffer.readVarInt(); + byte[] left = rawBuffer.readBytesLeft(); + if (packetSize == 0) { + // no need + data = left; + } else { + // need to decompress data + data = Util.decompress(left, connection).readBytesLeft(); + } + } + + InPacketBuffer inPacketBuffer = new InPacketBuffer(data, connection); + Packets.Clientbound packet = null; + try { + packet = connection.getPacketByCommand(connection.getConnectionState(), inPacketBuffer.getCommand()); + if (packet == null) { + Log.fatal(String.format("Version packet enum does not contain a packet with id 0x%x. Your version.json is broken!", inPacketBuffer.getCommand())); + System.exit(1); + } + Class clazz = packet.getClazz(); + + if (clazz == null) { + Log.warn(String.format("[IN] Received unknown packet (id=0x%x, name=%s, length=%d, dataLength=%d, version=%s, state=%s)", inPacketBuffer.getCommand(), packet, inPacketBuffer.getLength(), inPacketBuffer.getBytesLeft(), connection.getVersion(), connection.getConnectionState())); + return; + } + try { + ClientboundPacket packetInstance = clazz.getConstructor().newInstance(); + boolean success = packetInstance.read(inPacketBuffer); + if (inPacketBuffer.getBytesLeft() > 0 || !success) { + // warn not all data used + Log.warn(String.format("[IN] Could not parse packet %s (used=%d, available=%d, total=%d, success=%s)", packet, inPacketBuffer.getPosition(), inPacketBuffer.getBytesLeft(), inPacketBuffer.getLength(), success)); + return; + } + + //set special settings to avoid miss timing issues + if (packetInstance instanceof PacketLoginSuccess) { + connection.setConnectionState(ConnectionStates.PLAY); + } else if (packetInstance instanceof PacketCompressionInterface) { + nettyNetwork.compressionThreshold = ((PacketCompressionInterface) packetInstance).getThreshold(); + } else if (packetInstance instanceof PacketEncryptionRequest) { + // wait until response is ready + list.add(packetInstance); + return; + } + list.add(packetInstance); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + // safety first, but will not occur + e.printStackTrace(); + } + } catch (Exception e) { + Log.protocol(String.format("An error occurred while parsing an packet (%s): %s", packet, e)); + if (Log.getLevel().ordinal() >= LogLevels.DEBUG.ordinal()) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/netty/PacketEncoder.java b/src/main/java/de/bixilon/minosoft/protocol/network/netty/PacketEncoder.java new file mode 100644 index 000000000..35223b810 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/protocol/network/netty/PacketEncoder.java @@ -0,0 +1,70 @@ +/* + * Codename Minosoft + * Copyright (C) 2020 Moritz Zwerger + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.protocol.network.netty; + + +import de.bixilon.minosoft.protocol.network.Connection; +import de.bixilon.minosoft.protocol.packets.ServerboundPacket; +import de.bixilon.minosoft.protocol.packets.serverbound.login.PacketEncryptionResponse; +import de.bixilon.minosoft.protocol.protocol.OutByteBuffer; +import de.bixilon.minosoft.util.Util; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +public class PacketEncoder extends MessageToByteEncoder { + final Connection connection; + final NettyNetwork nettyNetwork; + + public PacketEncoder(Connection connection, NettyNetwork nettyNetwork) { + this.connection = connection; + this.nettyNetwork = nettyNetwork; + } + + @Override + protected void encode(ChannelHandlerContext channelHandlerContext, ServerboundPacket packet, ByteBuf byteBuf) throws Exception { + packet.log(); + byte[] data = packet.write(connection).getOutBytes(); + if (nettyNetwork.getCompressionThreshold() >= 0) { + // compression is enabled + // check if there is a need to compress it and if so, do it! + OutByteBuffer outRawBuffer = new OutByteBuffer(connection); + if (data.length >= nettyNetwork.getCompressionThreshold()) { + // compress it + OutByteBuffer compressedBuffer = new OutByteBuffer(connection); + byte[] compressed = Util.compress(data); + compressedBuffer.writeVarInt(data.length); + compressedBuffer.writeBytes(compressed); + outRawBuffer.writeVarInt(compressedBuffer.getOutBytes().length); + outRawBuffer.writeBytes(compressedBuffer.getOutBytes()); + } else { + outRawBuffer.writeVarInt(data.length + 1); // 1 for the compressed length (0) + outRawBuffer.writeVarInt(0); + outRawBuffer.writeBytes(data); + } + data = outRawBuffer.getOutBytes(); + } else { + // append packet length + OutByteBuffer bufferWithLengthPrefix = new OutByteBuffer(connection); + bufferWithLengthPrefix.writeVarInt(data.length); + bufferWithLengthPrefix.writeBytes(data); + data = bufferWithLengthPrefix.getOutBytes(); + } + byteBuf.writeBytes(data); + if (packet instanceof PacketEncryptionResponse) { + // enable encryption + nettyNetwork.enableEncryption(((PacketEncryptionResponse) packet).getSecretKey()); + } + } +} diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/netty/PacketReceiver.java b/src/main/java/de/bixilon/minosoft/protocol/network/netty/PacketReceiver.java new file mode 100644 index 000000000..d1a9b7633 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/protocol/network/netty/PacketReceiver.java @@ -0,0 +1,41 @@ +/* + * Codename Minosoft + * Copyright (C) 2020 Moritz Zwerger + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.protocol.network.netty; + +import de.bixilon.minosoft.logging.Log; +import de.bixilon.minosoft.logging.LogLevels; +import de.bixilon.minosoft.protocol.network.Connection; +import de.bixilon.minosoft.protocol.packets.ClientboundPacket; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +public class PacketReceiver extends SimpleChannelInboundHandler { + final Connection connection; + + public PacketReceiver(Connection connection) { + this.connection = connection; + } + + @Override + protected void channelRead0(ChannelHandlerContext channelHandlerContext, ClientboundPacket packet) { + try { + packet.log(); + packet.handle(connection.getHandler()); + } catch (Exception e) { + if (Log.getLevel().ordinal() >= LogLevels.DEBUG.ordinal()) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/netty/TCPClientChannelInitializer.java b/src/main/java/de/bixilon/minosoft/protocol/network/netty/TCPClientChannelInitializer.java new file mode 100644 index 000000000..ae4454ce9 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/protocol/network/netty/TCPClientChannelInitializer.java @@ -0,0 +1,41 @@ +/* + * Codename Minosoft + * Copyright (C) 2020 Moritz Zwerger + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.protocol.network.netty; + +import de.bixilon.minosoft.protocol.network.Connection; +import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.timeout.ReadTimeoutHandler; + +import java.util.concurrent.TimeUnit; + +public class TCPClientChannelInitializer extends ChannelInitializer { + final Connection connection; + final NettyNetwork nettyNetwork; + + public TCPClientChannelInitializer(Connection connection, NettyNetwork nettyNetwork) { + this.connection = connection; + this.nettyNetwork = nettyNetwork; + } + + @Override + protected void initChannel(NioSocketChannel socketChannel) { + nettyNetwork.setNioChannel(socketChannel); + socketChannel.pipeline().addLast("timeout", new ReadTimeoutHandler(ProtocolDefinition.SOCKET_TIMEOUT, TimeUnit.MILLISECONDS)); + socketChannel.pipeline().addLast("decoder", new PacketDecoder(connection, nettyNetwork)); + socketChannel.pipeline().addLast("encoder", new PacketEncoder(connection, nettyNetwork)); + socketChannel.pipeline().addLast(new PacketReceiver(connection)); + } +} diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/socket/SocketNetwork.java b/src/main/java/de/bixilon/minosoft/protocol/network/socket/SocketNetwork.java new file mode 100644 index 000000000..cdc31a3a7 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/protocol/network/socket/SocketNetwork.java @@ -0,0 +1,292 @@ +/* + * Codename Minosoft + * Copyright (C) 2020 Moritz Zwerger + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.protocol.network.socket; + +import de.bixilon.minosoft.logging.Log; +import de.bixilon.minosoft.logging.LogLevels; +import de.bixilon.minosoft.protocol.network.Connection; +import de.bixilon.minosoft.protocol.network.Network; +import de.bixilon.minosoft.protocol.packets.ClientboundPacket; +import de.bixilon.minosoft.protocol.packets.ServerboundPacket; +import de.bixilon.minosoft.protocol.packets.clientbound.interfaces.PacketCompressionInterface; +import de.bixilon.minosoft.protocol.packets.clientbound.login.PacketEncryptionRequest; +import de.bixilon.minosoft.protocol.packets.clientbound.login.PacketLoginSuccess; +import de.bixilon.minosoft.protocol.packets.serverbound.login.PacketEncryptionResponse; +import de.bixilon.minosoft.protocol.protocol.*; +import de.bixilon.minosoft.util.ServerAddress; +import de.bixilon.minosoft.util.Util; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.SecretKey; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketException; +import java.util.concurrent.LinkedBlockingQueue; + +public class SocketNetwork implements Network { + final Connection connection; + final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + Thread socketRThread; + Thread socketSThread; + int compressionThreshold = -1; + Socket socket; + OutputStream outputStream; + InputStream inputStream; + boolean encryptionEnabled = false; + SecretKey secretKey; + Exception lastException; + + public SocketNetwork(Connection connection) { + this.connection = connection; + } + + @Override + public void connect(ServerAddress address) { + // check if we are already connected or try to connect + if (connection.isConnected() || connection.getConnectionState() == ConnectionStates.CONNECTING) { + return; + } + // wait for data or send until it should disconnect + // first send, then receive + // something to send it, send it + // send, flush and remove + // everything sent for now, waiting for data + // add to queue + // Could not connect + socketRThread = new Thread(() -> { + try { + socket = new Socket(); + socket.setSoTimeout(ProtocolDefinition.SOCKET_CONNECT_TIMEOUT); + socket.connect(new InetSocketAddress(address.getHostname(), address.getPort()), ProtocolDefinition.SOCKET_CONNECT_TIMEOUT); + // connected, use minecraft timeout + socket.setSoTimeout(ProtocolDefinition.SOCKET_TIMEOUT); + connection.setConnectionState(ConnectionStates.HANDSHAKING); + socket.setKeepAlive(true); + outputStream = socket.getOutputStream(); + inputStream = socket.getInputStream(); + + socketRThread.setName(String.format("%d/SocketR", connection.getConnectionId())); + + socketSThread = new Thread(() -> { + try { + while (connection.getConnectionState() != ConnectionStates.DISCONNECTING) { + // wait for data or send until it should disconnect + + // check if still connected + if (!socket.isConnected() || socket.isClosed()) { + break; + } + + ServerboundPacket packet = queue.take(); + packet.log(); + queue.remove(packet); + byte[] data = packet.write(connection).getOutBytes(); + if (compressionThreshold >= 0) { + // compression is enabled + // check if there is a need to compress it and if so, do it! + OutByteBuffer outRawBuffer = new OutByteBuffer(connection); + if (data.length >= compressionThreshold) { + // compress it + OutByteBuffer compressedBuffer = new OutByteBuffer(connection); + byte[] compressed = Util.compress(data); + compressedBuffer.writeVarInt(data.length); + compressedBuffer.writeBytes(compressed); + outRawBuffer.writeVarInt(compressedBuffer.getOutBytes().length); + outRawBuffer.writeBytes(compressedBuffer.getOutBytes()); + } else { + outRawBuffer.writeVarInt(data.length + 1); // 1 for the compressed length (0) + outRawBuffer.writeVarInt(0); + outRawBuffer.writeBytes(data); + } + data = outRawBuffer.getOutBytes(); + } else { + // append packet length + OutByteBuffer bufferWithLengthPrefix = new OutByteBuffer(connection); + bufferWithLengthPrefix.writeVarInt(data.length); + bufferWithLengthPrefix.writeBytes(data); + data = bufferWithLengthPrefix.getOutBytes(); + } + + outputStream.write(data); + outputStream.flush(); + if (packet instanceof PacketEncryptionResponse) { + // enable encryption + secretKey = ((PacketEncryptionResponse) packet).getSecretKey(); + enableEncryption(secretKey); + // wake up other thread + socketRThread.interrupt(); + } + } + } catch (IOException | InterruptedException ignored) { + } + }); + socketSThread.setName(String.format("%d/SocketS", connection.getConnectionId())); + socketSThread.start(); + + while (connection.getConnectionState() != ConnectionStates.DISCONNECTING) { + // wait for data or send until it should disconnect + // first send, then receive + + // check if still connected + if (!socket.isConnected() || socket.isClosed()) { + break; + } + +// everything sent for now, waiting for data + int numRead = 0; + int length = 0; + int read; + do { + read = inputStream.read(); + if (read == -1) { + disconnect(); + return; + } + int value = (read & 0b01111111); + length |= (value << (7 * numRead)); + + numRead++; + if (numRead > 5) { + throw new RuntimeException("VarInt is too big"); + } + } while ((read & 0b10000000) != 0); + if (length > ProtocolDefinition.PROTOCOL_PACKET_MAX_SIZE) { + Log.protocol(String.format("Server sent us a to big packet (%d bytes > %d bytes)", length, ProtocolDefinition.PROTOCOL_PACKET_MAX_SIZE)); + inputStream.skip(length); + continue; + } + byte[] data = inputStream.readNBytes(length); + + if (compressionThreshold >= 0) { + // compression is enabled + // check if there is a need to decompress it and if so, do it! + InByteBuffer rawBuffer = new InByteBuffer(data, connection); + int packetSize = rawBuffer.readVarInt(); + byte[] left = rawBuffer.readBytesLeft(); + if (packetSize == 0) { + // no need + data = left; + } else { + // need to decompress data + data = Util.decompress(left, connection).readBytesLeft(); + } + } + + InPacketBuffer inPacketBuffer = new InPacketBuffer(data, connection); + Packets.Clientbound packet = null; + try { + packet = connection.getPacketByCommand(connection.getConnectionState(), inPacketBuffer.getCommand()); + if (packet == null) { + Log.fatal(String.format("Version packet enum does not contain a packet with id 0x%x. Your version.json is broken!", inPacketBuffer.getCommand())); + System.exit(1); + } + Class clazz = packet.getClazz(); + + if (clazz == null) { + Log.warn(String.format("[IN] Received unknown packet (id=0x%x, name=%s, length=%d, dataLength=%d, version=%s, state=%s)", inPacketBuffer.getCommand(), packet, inPacketBuffer.getLength(), inPacketBuffer.getBytesLeft(), connection.getVersion(), connection.getConnectionState())); + continue; + } + try { + ClientboundPacket packetInstance = clazz.getConstructor().newInstance(); + boolean success = packetInstance.read(inPacketBuffer); + if (inPacketBuffer.getBytesLeft() > 0 || !success) { + // warn not all data used + Log.warn(String.format("[IN] Could not parse packet %s (used=%d, available=%d, total=%d, success=%s)", packet, inPacketBuffer.getPosition(), inPacketBuffer.getBytesLeft(), inPacketBuffer.getLength(), success)); + continue; + } + + //set special settings to avoid miss timing issues + if (packetInstance instanceof PacketLoginSuccess) { + connection.setConnectionState(ConnectionStates.PLAY); + } else if (packetInstance instanceof PacketCompressionInterface) { + compressionThreshold = ((PacketCompressionInterface) packetInstance).getThreshold(); + } else if (packetInstance instanceof PacketEncryptionRequest) { + // wait until response is ready + connection.handle(packetInstance); + try { + Thread.sleep(Integer.MAX_VALUE); + } catch (InterruptedException ignored) { + } + continue; + } + connection.handle(packetInstance); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + // safety first, but will not occur + e.printStackTrace(); + } + } catch (Exception e) { + Log.protocol(String.format("An error occurred while parsing an packet (%s): %s", packet, e)); + if (Log.getLevel().ordinal() >= LogLevels.DEBUG.ordinal()) { + e.printStackTrace(); + } + } + } + disconnect(); + } catch (IOException e) { + // Could not connect + if (Log.getLevel().ordinal() >= LogLevels.DEBUG.ordinal()) { + e.printStackTrace(); + } + if (socketSThread != null) { + socketSThread.interrupt(); + } + if (e instanceof SocketException && e.getMessage().equals("Socket closed")) { + return; + } + lastException = e; + connection.setConnectionState(ConnectionStates.FAILED); + } + }); + socketRThread.setName(String.format("%d/Socket", connection.getConnectionId())); + socketRThread.start(); + } + + @Override + public void sendPacket(ServerboundPacket p) { + queue.add(p); + } + + private void enableEncryption(SecretKey secretKey) { + Cipher cipherEncrypt = CryptManager.createNetCipherInstance(Cipher.ENCRYPT_MODE, secretKey); + Cipher cipherDecrypt = CryptManager.createNetCipherInstance(Cipher.DECRYPT_MODE, secretKey); + inputStream = new CipherInputStream(inputStream, cipherDecrypt); + outputStream = new CipherOutputStream(outputStream, cipherEncrypt); + encryptionEnabled = true; + Log.debug("Encryption enabled!"); + } + + @Override + public void disconnect() { + connection.setConnectionState(ConnectionStates.DISCONNECTING); + try { + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + socketRThread.interrupt(); + socketSThread.interrupt(); + connection.setConnectionState(ConnectionStates.DISCONNECTED); + } + + @Override + public Exception getLastException() { + return lastException; + } +} diff --git a/src/main/java/de/bixilon/minosoft/util/Util.java b/src/main/java/de/bixilon/minosoft/util/Util.java index a7f2eb291..1a9dccf24 100644 --- a/src/main/java/de/bixilon/minosoft/util/Util.java +++ b/src/main/java/de/bixilon/minosoft/util/Util.java @@ -189,7 +189,7 @@ public final class Util { } public static ThreadFactory getThreadFactory(String threadName) { - return new ThreadFactoryBuilder().setNameFormat("%d/" + threadName).build(); + return new ThreadFactoryBuilder().setNameFormat(threadName + "#%d").build(); } public static void executeInThreadPool(String name, HashSet> callables) throws InterruptedException {