diff --git a/src/main/java/de/bixilon/minosoft/protocol/exceptions/PacketNotImplementedException.java b/src/main/java/de/bixilon/minosoft/protocol/exceptions/PacketNotImplementedException.java new file mode 100644 index 000000000..fff18def2 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/protocol/exceptions/PacketNotImplementedException.java @@ -0,0 +1,25 @@ +/* + * 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.exceptions; + +import de.bixilon.minosoft.protocol.network.Connection; +import de.bixilon.minosoft.protocol.protocol.InPacketBuffer; +import de.bixilon.minosoft.protocol.protocol.Packets; + +public class PacketNotImplementedException extends PacketParseException { + + public PacketNotImplementedException(InPacketBuffer buffer, Packets.Clientbound packetType, Connection connection) { + super(String.format("Packet not implemented yet (id=0x%x, name=%s, length=%d, dataLength=%d, version=%s, state=%s)", buffer.getCommand(), packetType, buffer.getLength(), buffer.getBytesLeft(), connection.getVersion(), connection.getConnectionState())); + } +} diff --git a/src/main/java/de/bixilon/minosoft/protocol/protocol/PacketParseException.java b/src/main/java/de/bixilon/minosoft/protocol/exceptions/PacketParseException.java similarity index 96% rename from src/main/java/de/bixilon/minosoft/protocol/protocol/PacketParseException.java rename to src/main/java/de/bixilon/minosoft/protocol/exceptions/PacketParseException.java index 7c68c0487..571aebba1 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/protocol/PacketParseException.java +++ b/src/main/java/de/bixilon/minosoft/protocol/exceptions/PacketParseException.java @@ -11,23 +11,23 @@ * This software is not affiliated with Mojang AB, the original developer of Minecraft. */ -package de.bixilon.minosoft.protocol.protocol; +package de.bixilon.minosoft.protocol.exceptions; public class PacketParseException extends Exception { - public PacketParseException() { + public PacketParseException(Throwable cause) { + super(cause); } public PacketParseException(String message) { super(message); } - public PacketParseException(String message, Throwable cause) { - super(message, cause); + public PacketParseException() { } - public PacketParseException(Throwable cause) { - super(cause); + public PacketParseException(String message, Throwable cause) { + super(message, cause); } public PacketParseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { diff --git a/src/main/java/de/bixilon/minosoft/protocol/exceptions/PacketTooLongException.java b/src/main/java/de/bixilon/minosoft/protocol/exceptions/PacketTooLongException.java new file mode 100644 index 000000000..5a58d85c0 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/protocol/exceptions/PacketTooLongException.java @@ -0,0 +1,23 @@ +/* + * 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.exceptions; + +import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition; + +public class PacketTooLongException extends PacketParseException { + + public PacketTooLongException(int length) { + super(String.format("Server sent us a to big packet (%d bytes > %d bytes)", length, ProtocolDefinition.PROTOCOL_PACKET_MAX_SIZE)); + } +} diff --git a/src/main/java/de/bixilon/minosoft/protocol/protocol/UnknownPacketException.java b/src/main/java/de/bixilon/minosoft/protocol/exceptions/UnknownPacketException.java similarity index 91% rename from src/main/java/de/bixilon/minosoft/protocol/protocol/UnknownPacketException.java rename to src/main/java/de/bixilon/minosoft/protocol/exceptions/UnknownPacketException.java index bc1fad805..f834f33bc 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/protocol/UnknownPacketException.java +++ b/src/main/java/de/bixilon/minosoft/protocol/exceptions/UnknownPacketException.java @@ -11,9 +11,9 @@ * This software is not affiliated with Mojang AB, the original developer of Minecraft. */ -package de.bixilon.minosoft.protocol.protocol; +package de.bixilon.minosoft.protocol.exceptions; -public class UnknownPacketException extends Exception { +public class UnknownPacketException extends PacketParseException { public UnknownPacketException() { } 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 index a1305de13..5b6118ed2 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/socket/SocketNetwork.java +++ b/src/main/java/de/bixilon/minosoft/protocol/network/socket/SocketNetwork.java @@ -15,6 +15,10 @@ package de.bixilon.minosoft.protocol.network.socket; import de.bixilon.minosoft.logging.Log; import de.bixilon.minosoft.logging.LogLevels; +import de.bixilon.minosoft.protocol.exceptions.PacketNotImplementedException; +import de.bixilon.minosoft.protocol.exceptions.PacketParseException; +import de.bixilon.minosoft.protocol.exceptions.PacketTooLongException; +import de.bixilon.minosoft.protocol.exceptions.UnknownPacketException; import de.bixilon.minosoft.protocol.network.Connection; import de.bixilon.minosoft.protocol.network.Network; import de.bixilon.minosoft.protocol.packets.ClientboundPacket; @@ -34,42 +38,53 @@ 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; - Throwable lastException; + private final Connection connection; + private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + private Thread socketReceiveThread; + private Thread socketSendThread; + private int compressionThreshold = -1; + private Socket socket; + private OutputStream outputStream; + private InputStream inputStream; + private Throwable lastException; public SocketNetwork(Connection connection) { this.connection = connection; } + private static int readStreamVarInt(InputStream inputStream) throws IOException { + int readCount = 0; + int varInt = 0; + int currentByte; + do { + currentByte = inputStream.read(); + if (currentByte == -1) { + throw new SocketException("Socket closed"); + } + int value = (currentByte & 0x7F); + varInt |= (value << (7 * readCount)); + + readCount++; + if (readCount > 5) { + throw new RuntimeException("VarInt is too big"); + } + } while ((currentByte & 0x80) != 0); + return varInt; + } + @Override public void connect(ServerAddress address) { this.lastException = null; - // check if we are already connected or try to connect if (this.connection.isConnected() || this.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 - this.socketRThread = new Thread(() -> { + this.socketReceiveThread = new Thread(() -> { try { this.socket = new Socket(); this.socket.setSoTimeout(ProtocolDefinition.SOCKET_CONNECT_TIMEOUT); @@ -81,178 +96,37 @@ public class SocketNetwork implements Network { this.outputStream = this.socket.getOutputStream(); this.inputStream = this.socket.getInputStream(); - this.socketRThread.setName(String.format("%d/SocketR", this.connection.getConnectionId())); - this.socketSThread = new Thread(() -> { - try { - while (this.connection.getConnectionState() != ConnectionStates.DISCONNECTING) { - // wait for data or send until it should disconnect + initSendThread(); - // check if still connected - if (!this.socket.isConnected() || this.socket.isClosed()) { - break; - } + this.socketReceiveThread.setName(String.format("%d/SocketR", this.connection.getConnectionId())); - ServerboundPacket packet = this.queue.take(); - packet.log(); - this.queue.remove(packet); - byte[] data = packet.write(this.connection).toByteArray(); - if (this.compressionThreshold >= 0) { - // compression is enabled - // check if there is a need to compress it and if so, do it! - OutByteBuffer outRawBuffer = new OutByteBuffer(this.connection); - if (data.length >= this.compressionThreshold) { - // compress it - OutByteBuffer lengthPrefixedBuffer = new OutByteBuffer(this.connection); - byte[] compressed = Util.compress(data); - lengthPrefixedBuffer.writeVarInt(data.length); // uncompressed length - lengthPrefixedBuffer.writeBytes(compressed); - outRawBuffer.prefixVarInt(lengthPrefixedBuffer.toByteArray().length); // length of total data is uncompressed length + compressed data - outRawBuffer.writeBytes(lengthPrefixedBuffer.toByteArray()); // write all bytes - } else { - outRawBuffer.writeVarInt(data.length + 1); // 1 for the compressed length (0) - outRawBuffer.writeVarInt(0); // data is uncompressed, compressed size is 0 - outRawBuffer.writeBytes(data); - } - data = outRawBuffer.toByteArray(); - } else { - // append packet length - OutByteBuffer bufferWithLengthPrefix = new OutByteBuffer(this.connection); - bufferWithLengthPrefix.writeVarInt(data.length); - bufferWithLengthPrefix.writeBytes(data); - data = bufferWithLengthPrefix.toByteArray(); - } - - this.outputStream.write(data); - this.outputStream.flush(); - if (packet instanceof PacketEncryptionResponse packetEncryptionResponse) { - // enable encryption - enableEncryption(packetEncryptionResponse.getSecretKey()); - // wake up other thread - this.socketRThread.interrupt(); - } - } - } catch (IOException | InterruptedException ignored) { - } - }, String.format("%d/SocketS", this.connection.getConnectionId())); - this.socketSThread.start(); while (this.connection.getConnectionState() != ConnectionStates.DISCONNECTING) { - // wait for data or send until it should disconnect - // first send, then receive - - // check if still connected if (!this.socket.isConnected() || this.socket.isClosed()) { break; } - - // everything sent for now, waiting for data - int numRead = 0; - int length = 0; - int read; - do { - read = this.inputStream.read(); - if (read == -1) { - disconnect(); - return; - } - int value = (read & 0x7F); - length |= (value << (7 * numRead)); - - numRead++; - if (numRead > 5) { - throw new RuntimeException("VarInt is too big"); - } - } while ((read & 0x80) != 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)); - this.inputStream.skip(length); - continue; - } - byte[] data = this.inputStream.readNBytes(length); - - if (this.compressionThreshold >= 0) { - // compression is enabled - // check if there is a need to decompress it and if so, do it! - InByteBuffer rawBuffer = new InByteBuffer(data, this.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, this.connection).readBytesLeft(); - } - } - - InPacketBuffer inPacketBuffer = new InPacketBuffer(data, this.connection); - Packets.Clientbound packet = null; try { - packet = this.connection.getPacketByCommand(this.connection.getConnectionState(), inPacketBuffer.getCommand()); - if (packet == null) { - disconnect(); - this.lastException = new UnknownPacketException(String.format("Server sent us an unknown packet (id=0x%x, length=%d, data=%s)", inPacketBuffer.getCommand(), length, inPacketBuffer.getBase64())); - throw this.lastException; - } - Class clazz = packet.getClazz(); - - if (clazz == null) { - throw new UnknownPacketException(String.format("Packet not implemented yet (id=0x%x, name=%s, length=%d, dataLength=%d, version=%s, state=%s)", inPacketBuffer.getCommand(), packet, inPacketBuffer.getLength(), inPacketBuffer.getBytesLeft(), this.connection.getVersion(), this.connection.getConnectionState())); - } - try { - ClientboundPacket packetInstance = clazz.getConstructor().newInstance(); - boolean success = packetInstance.read(inPacketBuffer); - if (inPacketBuffer.getBytesLeft() > 0 || !success) { - throw new PacketParseException(String.format("Could not parse packet %s (used=%d, available=%d, total=%d, success=%s)", packet, inPacketBuffer.getPosition(), inPacketBuffer.getBytesLeft(), inPacketBuffer.getLength(), success)); - } - - // set special settings to avoid miss timing issues - if (packetInstance instanceof PacketLoginSuccess) { - this.connection.setConnectionState(ConnectionStates.PLAY); - } else if (packetInstance instanceof CompressionThresholdChange compressionPacket) { - this.compressionThreshold = compressionPacket.getThreshold(); - } else if (packetInstance instanceof PacketEncryptionRequest) { - // wait until response is ready - this.connection.handle(packetInstance); - try { - Thread.sleep(Integer.MAX_VALUE); - } catch (InterruptedException ignored) { - } - continue; - } - this.connection.handle(packetInstance); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - // safety first, but will not occur - e.printStackTrace(); - } - } catch (Throwable e) { - Log.protocol(String.format("An error occurred while parsing a packet (%s): %s", packet, e)); - if (this.connection.getConnectionState() == ConnectionStates.PLAY) { - Log.printException(e, LogLevels.PROTOCOL); - continue; - } - this.lastException = e; - disconnect(); - this.connection.setConnectionState(ConnectionStates.FAILED); - throw new RuntimeException(e); + handlePacket(receiveClientboundPacket(this.inputStream)); + } catch (PacketParseException e) { + Log.printException(e, LogLevels.PROTOCOL); } } disconnect(); - } catch (IOException e) { + } catch (Exception e) { // Could not connect - Log.printException(e, LogLevels.PROTOCOL); - if (this.socketSThread != null) { - this.socketSThread.interrupt(); + if (this.socketSendThread != null) { + this.socketSendThread.interrupt(); } if (e instanceof SocketException && e.getMessage().equals("Socket closed")) { return; } + Log.printException(e, LogLevels.PROTOCOL); this.lastException = e; this.connection.setConnectionState(ConnectionStates.FAILED); } }, String.format("%d/Socket", this.connection.getConnectionId())); - this.socketRThread.start(); + this.socketReceiveThread.start(); } @Override @@ -268,8 +142,8 @@ public class SocketNetwork implements Network { } catch (IOException e) { e.printStackTrace(); } - this.socketRThread.interrupt(); - this.socketSThread.interrupt(); + this.socketReceiveThread.interrupt(); + this.socketSendThread.interrupt(); this.connection.setConnectionState(ConnectionStates.DISCONNECTED); } @@ -278,11 +152,133 @@ public class SocketNetwork implements Network { return this.lastException; } + private void initSendThread() { + this.socketSendThread = new Thread(() -> { + try { + while (this.connection.getConnectionState() != ConnectionStates.DISCONNECTING) { + // wait for data or send until it should disconnect + + // check if still connected + if (!this.socket.isConnected() || this.socket.isClosed()) { + break; + } + + ServerboundPacket packet = this.queue.take(); + packet.log(); + + this.outputStream.write(prepareServerboundPacket(packet)); + this.outputStream.flush(); + if (packet instanceof PacketEncryptionResponse packetEncryptionResponse) { + // enable encryption + enableEncryption(packetEncryptionResponse.getSecretKey()); + // wake up other thread + this.socketReceiveThread.interrupt(); + } + } + } catch (IOException | InterruptedException ignored) { + } + }, String.format("%d/SocketS", this.connection.getConnectionId())); + this.socketSendThread.start(); + } + + private byte[] prepareServerboundPacket(ServerboundPacket packet) { + byte[] data = packet.write(this.connection).toByteArray(); + if (this.compressionThreshold >= 0) { + // compression is enabled + // check if there is a need to compress it and if so, do it! + OutByteBuffer outRawBuffer = new OutByteBuffer(this.connection); + if (data.length >= this.compressionThreshold) { + // compress it + OutByteBuffer lengthPrefixedBuffer = new OutByteBuffer(this.connection); + byte[] compressed = Util.compress(data); + lengthPrefixedBuffer.writeVarInt(data.length); // uncompressed length + lengthPrefixedBuffer.writeBytes(compressed); + outRawBuffer.prefixVarInt(lengthPrefixedBuffer.toByteArray().length); // length of total data is uncompressed length + compressed data + outRawBuffer.writeBytes(lengthPrefixedBuffer.toByteArray()); // write all bytes + } else { + outRawBuffer.writeVarInt(data.length + 1); // 1 for the compressed length (0) + outRawBuffer.writeVarInt(0); // data is uncompressed, compressed size is 0 + outRawBuffer.writeBytes(data); + } + data = outRawBuffer.toByteArray(); + } else { + // append packet length + OutByteBuffer bufferWithLengthPrefix = new OutByteBuffer(this.connection); + bufferWithLengthPrefix.writeVarInt(data.length); + bufferWithLengthPrefix.writeBytes(data); + data = bufferWithLengthPrefix.toByteArray(); + } + return data; + } + + private ClientboundPacket receiveClientboundPacket(InputStream inputStream) throws IOException, PacketParseException { + int packetLength = readStreamVarInt(inputStream); + + if (packetLength > ProtocolDefinition.PROTOCOL_PACKET_MAX_SIZE) { + inputStream.skip(packetLength); + throw new PacketTooLongException(packetLength); + } + + byte[] bytes = this.inputStream.readNBytes(packetLength); + if (this.compressionThreshold >= 0) { + // compression is enabled + InByteBuffer rawData = new InByteBuffer(bytes, this.connection); + int packetSize = rawData.readVarInt(); + if (packetSize > 0) { + // need to decompress data + bytes = Util.decompress(rawData.readBytesLeft()); + } + } + InPacketBuffer data = new InPacketBuffer(bytes, this.connection); + + Packets.Clientbound packetType = null; + + try { + packetType = this.connection.getPacketByCommand(this.connection.getConnectionState(), data.getCommand()); + if (packetType == null) { + throw new UnknownPacketException(String.format("Server sent us an unknown packet (id=0x%x, length=%d, data=%s)", data.getCommand(), packetLength, data.getBase64())); + } + Class clazz = packetType.getClazz(); + + if (clazz == null) { + throw new PacketNotImplementedException(data, packetType, this.connection); + } + ClientboundPacket packet = clazz.getConstructor().newInstance(); + boolean success = packet.read(data); + if (data.getBytesLeft() > 0 || !success) { + throw new PacketParseException(String.format("Could not parse packet %s (used=%d, available=%d, total=%d, success=%s)", packetType, data.getPosition(), data.getBytesLeft(), data.getLength(), success)); + } + return packet; + } catch (Throwable e) { + Log.protocol(String.format("An error occurred while parsing a packet (%s): %s", packetType, e)); + if (this.connection.getConnectionState() == ConnectionStates.PLAY) { + throw new PacketParseException(e); + } + throw new RuntimeException(e); + } + } + + private void handlePacket(ClientboundPacket packet) { + // set special settings to avoid miss timing issues + if (packet instanceof PacketLoginSuccess) { + this.connection.setConnectionState(ConnectionStates.PLAY); + } else if (packet instanceof CompressionThresholdChange compressionPacket) { + this.compressionThreshold = compressionPacket.getThreshold(); + } else if (packet instanceof PacketEncryptionRequest) { + // wait until response is ready + this.connection.handle(packet); + try { + Thread.sleep(Integer.MAX_VALUE); + } catch (InterruptedException ignored) { + } + return; + } + this.connection.handle(packet); + } + private void enableEncryption(SecretKey secretKey) { - Cipher cipherEncrypt = CryptManager.createNetCipherInstance(Cipher.ENCRYPT_MODE, secretKey); - Cipher cipherDecrypt = CryptManager.createNetCipherInstance(Cipher.DECRYPT_MODE, secretKey); - this.inputStream = new CipherInputStream(this.inputStream, cipherDecrypt); - this.outputStream = new CipherOutputStream(this.outputStream, cipherEncrypt); + this.inputStream = new CipherInputStream(this.inputStream, CryptManager.createNetCipherInstance(Cipher.DECRYPT_MODE, secretKey)); + this.outputStream = new CipherOutputStream(this.outputStream, CryptManager.createNetCipherInstance(Cipher.ENCRYPT_MODE, secretKey)); Log.debug("Encryption enabled!"); } }