Improve SocketNetwork.java

This commit is contained in:
Bixilon 2020-12-15 18:05:47 +01:00
parent 96db60d8d5
commit a182e4dbfe
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
5 changed files with 228 additions and 184 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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()));
}
}

View File

@ -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) {

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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));
}
}

View File

@ -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() {
}

View File

@ -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<ServerboundPacket> 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<ServerboundPacket> 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!");
}
}