mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-14 09:56:37 -04:00
wip netty rewrite
This commit is contained in:
parent
e5092c4245
commit
6703c58295
6
pom.xml
6
pom.xml
@ -119,5 +119,11 @@
|
|||||||
<artifactId>jcl-core</artifactId>
|
<artifactId>jcl-core</artifactId>
|
||||||
<version>2.8</version>
|
<version>2.8</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.netty</groupId>
|
||||||
|
<artifactId>netty-all</artifactId>
|
||||||
|
<version>4.1.52.Final</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
@ -17,15 +17,7 @@ import de.bixilon.minosoft.modding.event.EventManager;
|
|||||||
import de.bixilon.minosoft.modding.loading.ModInfo;
|
import de.bixilon.minosoft.modding.loading.ModInfo;
|
||||||
import de.bixilon.minosoft.modding.loading.ModPhases;
|
import de.bixilon.minosoft.modding.loading.ModPhases;
|
||||||
|
|
||||||
interface MinosoftModInterface {
|
public abstract class MinosoftMod {
|
||||||
/**
|
|
||||||
* @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 {
|
|
||||||
private final EventManager eventManager = new EventManager();
|
private final EventManager eventManager = new EventManager();
|
||||||
protected boolean enabled = true;
|
protected boolean enabled = true;
|
||||||
private ModInfo info;
|
private ModInfo info;
|
||||||
@ -58,4 +50,10 @@ public abstract class MinosoftMod implements MinosoftModInterface {
|
|||||||
public Logger getLogger() {
|
public Logger getLogger() {
|
||||||
return logger;
|
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);
|
||||||
}
|
}
|
@ -25,7 +25,6 @@ import de.bixilon.minosoft.game.datatypes.objectLoader.versions.Versions;
|
|||||||
import de.bixilon.minosoft.gui.main.ConnectionChangeCallback;
|
import de.bixilon.minosoft.gui.main.ConnectionChangeCallback;
|
||||||
import de.bixilon.minosoft.logging.Log;
|
import de.bixilon.minosoft.logging.Log;
|
||||||
import de.bixilon.minosoft.logging.LogLevels;
|
import de.bixilon.minosoft.logging.LogLevels;
|
||||||
import de.bixilon.minosoft.modding.event.EventListener;
|
|
||||||
import de.bixilon.minosoft.modding.event.EventManager;
|
import de.bixilon.minosoft.modding.event.EventManager;
|
||||||
import de.bixilon.minosoft.ping.ServerListPing;
|
import de.bixilon.minosoft.ping.ServerListPing;
|
||||||
import de.bixilon.minosoft.protocol.modding.channels.DefaultPluginChannels;
|
import de.bixilon.minosoft.protocol.modding.channels.DefaultPluginChannels;
|
||||||
@ -50,7 +49,7 @@ import java.util.concurrent.LinkedBlockingQueue;
|
|||||||
|
|
||||||
public class Connection {
|
public class Connection {
|
||||||
public static int lastConnectionId;
|
public static int lastConnectionId;
|
||||||
final Network network = new Network(this);
|
final Network network = Network.getNetworkInstance(this);
|
||||||
final PacketHandler handler = new PacketHandler(this);
|
final PacketHandler handler = new PacketHandler(this);
|
||||||
final PacketSender sender = new PacketSender(this);
|
final PacketSender sender = new PacketSender(this);
|
||||||
final LinkedBlockingQueue<ClientboundPacket> handlingQueue = new LinkedBlockingQueue<>();
|
final LinkedBlockingQueue<ClientboundPacket> handlingQueue = new LinkedBlockingQueue<>();
|
||||||
@ -73,6 +72,7 @@ public class Connection {
|
|||||||
ConnectionReasons nextReason;
|
ConnectionReasons nextReason;
|
||||||
ConnectionPing connectionStatusPing;
|
ConnectionPing connectionStatusPing;
|
||||||
ServerListPing lastPing;
|
ServerListPing lastPing;
|
||||||
|
Exception lastException;
|
||||||
|
|
||||||
public Connection(int connectionId, String hostname, Player player) {
|
public Connection(int connectionId, String hostname, Player player) {
|
||||||
this.connectionId = connectionId;
|
this.connectionId = connectionId;
|
||||||
@ -81,7 +81,7 @@ public class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void resolve(ConnectionReasons reason, int protocolId) {
|
public void resolve(ConnectionReasons reason, int protocolId) {
|
||||||
network.lastException = null;
|
lastException = null;
|
||||||
this.desiredVersionNumber = protocolId;
|
this.desiredVersionNumber = protocolId;
|
||||||
|
|
||||||
Thread resolveThread = new Thread(() -> {
|
Thread resolveThread = new Thread(() -> {
|
||||||
@ -93,7 +93,7 @@ public class Connection {
|
|||||||
addresses = DNSUtil.getServerAddresses(hostname);
|
addresses = DNSUtil.getServerAddresses(hostname);
|
||||||
} catch (TextParseException e) {
|
} catch (TextParseException e) {
|
||||||
setConnectionState(ConnectionStates.FAILED_NO_RETRY);
|
setConnectionState(ConnectionStates.FAILED_NO_RETRY);
|
||||||
network.lastException = e;
|
lastException = e;
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -232,7 +232,7 @@ public class Connection {
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
Log.fatal(String.format("Could not load mapping for %s. This version seems to be unsupported!", version));
|
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);
|
setConnectionState(ConnectionStates.FAILED_NO_RETRY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -396,7 +396,7 @@ public class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Exception getLastConnectionException() {
|
public Exception getLastConnectionException() {
|
||||||
return network.lastException;
|
return (lastException != null) ? lastException : network.getLastException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addConnectionChangeCallback(ConnectionChangeCallback callback) {
|
public void addConnectionChangeCallback(ConnectionChangeCallback callback) {
|
||||||
@ -422,8 +422,4 @@ public class Connection {
|
|||||||
public void unregisterEvents(EventManager... eventManagers) {
|
public void unregisterEvents(EventManager... eventManagers) {
|
||||||
this.eventManagers.removeAll(Arrays.asList(eventManagers));
|
this.eventManagers.removeAll(Arrays.asList(eventManagers));
|
||||||
}
|
}
|
||||||
|
|
||||||
public HashSet<EventListener> getAllEvents() {
|
|
||||||
return eventManagers;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -13,270 +13,20 @@
|
|||||||
|
|
||||||
package de.bixilon.minosoft.protocol.network;
|
package de.bixilon.minosoft.protocol.network;
|
||||||
|
|
||||||
import de.bixilon.minosoft.logging.Log;
|
import de.bixilon.minosoft.protocol.network.socket.SocketNetwork;
|
||||||
import de.bixilon.minosoft.logging.LogLevels;
|
|
||||||
import de.bixilon.minosoft.protocol.packets.ClientboundPacket;
|
|
||||||
import de.bixilon.minosoft.protocol.packets.ServerboundPacket;
|
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.ServerAddress;
|
||||||
import de.bixilon.minosoft.util.Util;
|
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
public interface Network {
|
||||||
import javax.crypto.CipherInputStream;
|
static Network getNetworkInstance(Connection connection) {
|
||||||
import javax.crypto.CipherOutputStream;
|
return new SocketNetwork(connection);
|
||||||
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<ServerboundPacket> 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 void connect(ServerAddress address) {
|
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()));
|
void sendPacket(ServerboundPacket packet);
|
||||||
|
|
||||||
socketSThread = new Thread(() -> {
|
void disconnect();
|
||||||
try {
|
|
||||||
while (connection.getConnectionState() != ConnectionStates.DISCONNECTING) {
|
|
||||||
// wait for data or send until it should disconnect
|
|
||||||
|
|
||||||
// check if still connected
|
Exception getLastException();
|
||||||
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<? extends ClientboundPacket> 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* 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<Object> 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<? extends ClientboundPacket> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* 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<ServerboundPacket> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* 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<ClientboundPacket> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* 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<NioSocketChannel> {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* 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<ServerboundPacket> 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<? extends ClientboundPacket> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -189,7 +189,7 @@ public final class Util {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static ThreadFactory getThreadFactory(String threadName) {
|
public static ThreadFactory getThreadFactory(String threadName) {
|
||||||
return new ThreadFactoryBuilder().setNameFormat("%d/" + threadName).build();
|
return new ThreadFactoryBuilder().setNameFormat(threadName + "#%d").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T> void executeInThreadPool(String name, HashSet<Callable<T>> callables) throws InterruptedException {
|
public static <T> void executeInThreadPool(String name, HashSet<Callable<T>> callables) throws InterruptedException {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user