mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-15 10:25:06 -04:00
Improve SocketNetwork.java
This commit is contained in:
parent
96db60d8d5
commit
a182e4dbfe
@ -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()));
|
||||
}
|
||||
}
|
@ -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) {
|
@ -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));
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
@ -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,9 +96,64 @@ 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(() -> {
|
||||
initSendThread();
|
||||
|
||||
this.socketReceiveThread.setName(String.format("%d/SocketR", this.connection.getConnectionId()));
|
||||
|
||||
|
||||
while (this.connection.getConnectionState() != ConnectionStates.DISCONNECTING) {
|
||||
if (!this.socket.isConnected() || this.socket.isClosed()) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
handlePacket(receiveClientboundPacket(this.inputStream));
|
||||
} catch (PacketParseException e) {
|
||||
Log.printException(e, LogLevels.PROTOCOL);
|
||||
}
|
||||
}
|
||||
disconnect();
|
||||
} catch (Exception e) {
|
||||
// Could not connect
|
||||
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.socketReceiveThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(ServerboundPacket packet) {
|
||||
this.queue.add(packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
this.connection.setConnectionState(ConnectionStates.DISCONNECTING);
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
this.socketReceiveThread.interrupt();
|
||||
this.socketSendThread.interrupt();
|
||||
this.connection.setConnectionState(ConnectionStates.DISCONNECTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable getLastException() {
|
||||
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
|
||||
@ -95,7 +165,23 @@ public class SocketNetwork implements Network {
|
||||
|
||||
ServerboundPacket packet = this.queue.take();
|
||||
packet.log();
|
||||
this.queue.remove(packet);
|
||||
|
||||
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
|
||||
@ -122,167 +208,77 @@ public class SocketNetwork implements Network {
|
||||
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;
|
||||
return data;
|
||||
}
|
||||
|
||||
// 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));
|
||||
private ClientboundPacket receiveClientboundPacket(InputStream inputStream) throws IOException, PacketParseException {
|
||||
int packetLength = readStreamVarInt(inputStream);
|
||||
|
||||
numRead++;
|
||||
if (numRead > 5) {
|
||||
throw new RuntimeException("VarInt is too big");
|
||||
if (packetLength > ProtocolDefinition.PROTOCOL_PACKET_MAX_SIZE) {
|
||||
inputStream.skip(packetLength);
|
||||
throw new PacketTooLongException(packetLength);
|
||||
}
|
||||
} 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);
|
||||
|
||||
byte[] bytes = this.inputStream.readNBytes(packetLength);
|
||||
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 {
|
||||
InByteBuffer rawData = new InByteBuffer(bytes, this.connection);
|
||||
int packetSize = rawData.readVarInt();
|
||||
if (packetSize > 0) {
|
||||
// need to decompress data
|
||||
data = Util.decompress(left, this.connection).readBytesLeft();
|
||||
bytes = Util.decompress(rawData.readBytesLeft());
|
||||
}
|
||||
}
|
||||
InPacketBuffer data = new InPacketBuffer(bytes, this.connection);
|
||||
|
||||
Packets.Clientbound packetType = null;
|
||||
|
||||
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;
|
||||
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 = packet.getClazz();
|
||||
Class<? extends ClientboundPacket> clazz = packetType.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()));
|
||||
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);
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
private void handlePacket(ClientboundPacket packet) {
|
||||
// set special settings to avoid miss timing issues
|
||||
if (packetInstance instanceof PacketLoginSuccess) {
|
||||
if (packet instanceof PacketLoginSuccess) {
|
||||
this.connection.setConnectionState(ConnectionStates.PLAY);
|
||||
} else if (packetInstance instanceof CompressionThresholdChange compressionPacket) {
|
||||
} else if (packet instanceof CompressionThresholdChange compressionPacket) {
|
||||
this.compressionThreshold = compressionPacket.getThreshold();
|
||||
} else if (packetInstance instanceof PacketEncryptionRequest) {
|
||||
} else if (packet instanceof PacketEncryptionRequest) {
|
||||
// wait until response is ready
|
||||
this.connection.handle(packetInstance);
|
||||
this.connection.handle(packet);
|
||||
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);
|
||||
}
|
||||
}
|
||||
disconnect();
|
||||
} catch (IOException e) {
|
||||
// Could not connect
|
||||
Log.printException(e, LogLevels.PROTOCOL);
|
||||
if (this.socketSThread != null) {
|
||||
this.socketSThread.interrupt();
|
||||
}
|
||||
if (e instanceof SocketException && e.getMessage().equals("Socket closed")) {
|
||||
return;
|
||||
}
|
||||
this.lastException = e;
|
||||
this.connection.setConnectionState(ConnectionStates.FAILED);
|
||||
}
|
||||
}, String.format("%d/Socket", this.connection.getConnectionId()));
|
||||
this.socketRThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(ServerboundPacket packet) {
|
||||
this.queue.add(packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
this.connection.setConnectionState(ConnectionStates.DISCONNECTING);
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
this.socketRThread.interrupt();
|
||||
this.socketSThread.interrupt();
|
||||
this.connection.setConnectionState(ConnectionStates.DISCONNECTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable getLastException() {
|
||||
return this.lastException;
|
||||
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!");
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user