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 extends ClientboundPacket> 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 extends ClientboundPacket> 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!");
}
}