diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml index b00065041..7c4f4954b 100644 --- a/.idea/jarRepositories.xml +++ b/.idea/jarRepositories.xml @@ -1,35 +1,35 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index b52f2e34e..0d5610083 100644 --- a/pom.xml +++ b/pom.xml @@ -26,8 +26,8 @@ org.apache.maven.plugins maven-compiler-plugin - 11 - 11 + 14 + 14 @@ -54,5 +54,11 @@ guava 29.0-jre + + + dnsjava + dnsjava + 3.2.2 + \ No newline at end of file diff --git a/src/main/java/de/bixilon/minosoft/Minosoft.java b/src/main/java/de/bixilon/minosoft/Minosoft.java index 706e2f0ce..a32af1446 100644 --- a/src/main/java/de/bixilon/minosoft/Minosoft.java +++ b/src/main/java/de/bixilon/minosoft/Minosoft.java @@ -21,6 +21,7 @@ import de.bixilon.minosoft.logging.Log; import de.bixilon.minosoft.logging.LogLevel; import de.bixilon.minosoft.mojang.api.MojangAccount; import de.bixilon.minosoft.protocol.network.Connection; +import de.bixilon.minosoft.protocol.protocol.ConnectionReason; import de.bixilon.minosoft.util.FolderUtil; import de.bixilon.minosoft.util.OSUtil; import de.bixilon.minosoft.util.Util; @@ -67,7 +68,7 @@ public class Minosoft { checkClientToken(); - Connection c = new Connection(config.getString("debug.host"), config.getInteger("debug.port")); + Connection c = new Connection(config.getString("debug.host")); accountList = config.getMojangAccounts(); if (accountList.size() == 0) { /* @@ -84,7 +85,7 @@ public class Minosoft { Log.mojang("Could not refresh session, you will not be able to join premium servers!"); } c.setPlayer(new Player(account)); - c.connect(); + c.resolve(ConnectionReason.CONNECT); // resolve dns address and connect } /** diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/Connection.java b/src/main/java/de/bixilon/minosoft/protocol/network/Connection.java index 852417d8d..2ecc6c324 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/Connection.java +++ b/src/main/java/de/bixilon/minosoft/protocol/network/Connection.java @@ -32,17 +32,20 @@ import de.bixilon.minosoft.protocol.packets.serverbound.login.PacketLoginStart; import de.bixilon.minosoft.protocol.packets.serverbound.status.PacketStatusPing; import de.bixilon.minosoft.protocol.packets.serverbound.status.PacketStatusRequest; import de.bixilon.minosoft.protocol.protocol.*; +import de.bixilon.minosoft.util.DNSUtil; +import de.bixilon.minosoft.util.ServerAddress; +import org.xbill.DNS.TextParseException; import java.util.ArrayList; public class Connection { - final String host; - final int port; + final ArrayList addresses; final Network network; final PacketHandler handler; final PacketSender sender; final ArrayList handlingQueue; final VelocityHandler velocityHandler = new VelocityHandler(this); + ServerAddress address; PluginChannelHandler pluginChannelHandler; Thread handleThread; Version version = Versions.getLowestVersionSupported(); // default @@ -50,11 +53,16 @@ public class Connection { Player player; ConnectionState state = ConnectionState.DISCONNECTED; ConnectionReason reason; + ConnectionReason nextReason; ConnectionPing connectionStatusPing; - public Connection(String host, int port) { - this.host = host; - this.port = port; + public Connection(String hostname) { + try { + addresses = DNSUtil.getServerAddresses(hostname); + } catch (TextParseException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } network = new Network(this); handlingQueue = new ArrayList<>(); handler = new PacketHandler(this); @@ -65,29 +73,41 @@ public class Connection { * Sends an server ping to the server (player count, motd, ...) */ public void ping() { - Log.info(String.format("Pinging server: %s:%d", host, port)); + Log.info(String.format("Pinging server: %s", address)); reason = ConnectionReason.PING; - network.connect(); + network.connect(address); + } + + public void resolve(ConnectionReason reason) { + address = addresses.get(0); + this.nextReason = reason; + Log.info(String.format("Trying to connect to %s", address)); + resolve(address); + } + + public void resolve(ServerAddress address) { + reason = ConnectionReason.DNS; + network.connect(address); } /** * Tries to connect to the server and login */ public void connect() { - Log.info(String.format("Connecting to server: %s:%d", host, port)); - if (reason == null) { + Log.info(String.format("Connecting to server: %s", address)); + if (reason == null || reason == ConnectionReason.DNS) { // first get version, then login reason = ConnectionReason.GET_VERSION; } - network.connect(); + network.connect(address); } - public String getHost() { - return host; + public ServerAddress getAddress() { + return address; } - public int getPort() { - return port; + public ArrayList getAvailableAddresses() { + return addresses; } public ConnectionState getConnectionState() { @@ -105,7 +125,17 @@ public class Connection { // connection established, starting threads and logging in startHandlingThread(); ConnectionState next = ((reason == ConnectionReason.CONNECT) ? ConnectionState.LOGIN : ConnectionState.STATUS); - network.sendPacket(new PacketHandshake(getHost(), getPort(), next, (next == ConnectionState.STATUS) ? -1 : getVersion().getProtocolVersion())); + if (reason == ConnectionReason.DNS) { + // valid hostname found + if (nextReason == ConnectionReason.CONNECT) { + // connecting, we must get the version first + reason = ConnectionReason.GET_VERSION; + } else { + reason = nextReason; + } + Log.info(String.format("Connection to %s seems to be okay, connecting...", address)); + } + network.sendPacket(new PacketHandshake(address, next, (next == ConnectionState.STATUS) ? -1 : getVersion().getProtocolVersion())); // after sending it, switch to next state setConnectionState(next); break; @@ -128,6 +158,18 @@ public class Connection { // unregister all custom recipes Recipes.removeCustomRecipes(); } + break; + case FAILED: + // connect to next hostname, if available + int nextIndex = addresses.indexOf(address) + 1; + if (addresses.size() > nextIndex) { + ServerAddress nextAddress = addresses.get(nextIndex); + Log.warn(String.format("Could not connect to %s, trying next hostname: %s", address, nextAddress)); + this.address = nextAddress; + resolve(address); + } + // else: failed + break; } } diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/Network.java b/src/main/java/de/bixilon/minosoft/protocol/network/Network.java index 5b6565cd3..12168e2ff 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/Network.java +++ b/src/main/java/de/bixilon/minosoft/protocol/network/Network.java @@ -20,6 +20,7 @@ import de.bixilon.minosoft.protocol.packets.clientbound.interfaces.PacketCompres 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; @@ -30,7 +31,9 @@ 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.SocketTimeoutException; import java.util.ArrayList; public class Network { @@ -51,7 +54,7 @@ public class Network { this.queue = new ArrayList<>(); } - public void connect() { + public void connect(ServerAddress address) { // wait for data or send until it should disconnect // first send, then receive // something to send it, send it @@ -61,7 +64,9 @@ public class Network { // Could not connect socketThread = new Thread(() -> { try { - socket = new Socket(connection.getHost(), connection.getPort()); + socket = new Socket(); + socket.setSoTimeout(ProtocolDefinition.SOCKET_CONNECT_TIMEOUT); + socket.connect(new InetSocketAddress(address.getHostname(), address.getPort()), ProtocolDefinition.SOCKET_CONNECT_TIMEOUT); connected = true; connection.setConnectionState(ConnectionState.HANDSHAKING); socket.setKeepAlive(true); @@ -197,7 +202,9 @@ public class Network { connection.setConnectionState(ConnectionState.DISCONNECTED); } catch (IOException e) { // Could not connect - connection.setConnectionState(ConnectionState.DISCONNECTED); + if (e instanceof SocketTimeoutException) { + connection.setConnectionState(ConnectionState.FAILED); + } e.printStackTrace(); } }); diff --git a/src/main/java/de/bixilon/minosoft/protocol/packets/serverbound/handshaking/PacketHandshake.java b/src/main/java/de/bixilon/minosoft/protocol/packets/serverbound/handshaking/PacketHandshake.java index bb47e7075..826fd8829 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/packets/serverbound/handshaking/PacketHandshake.java +++ b/src/main/java/de/bixilon/minosoft/protocol/packets/serverbound/handshaking/PacketHandshake.java @@ -19,26 +19,23 @@ import de.bixilon.minosoft.protocol.packets.ServerboundPacket; import de.bixilon.minosoft.protocol.protocol.ConnectionState; import de.bixilon.minosoft.protocol.protocol.OutPacketBuffer; import de.bixilon.minosoft.protocol.protocol.Packets; -import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition; +import de.bixilon.minosoft.util.ServerAddress; public class PacketHandshake implements ServerboundPacket { - final String address; - final int port; + final ServerAddress address; final ConnectionState nextState; final int version; - public PacketHandshake(String address, int port, ConnectionState nextState, int version) { + public PacketHandshake(ServerAddress address, ConnectionState nextState, int version) { this.address = address; - this.port = port; this.nextState = nextState; this.version = version; } - public PacketHandshake(String address, int version) { + public PacketHandshake(ServerAddress address, int version) { this.address = address; this.version = version; - this.port = ProtocolDefinition.DEFAULT_PORT; this.nextState = ConnectionState.STATUS; } @@ -46,14 +43,14 @@ public class PacketHandshake implements ServerboundPacket { public OutPacketBuffer write(Connection connection) { OutPacketBuffer buffer = new OutPacketBuffer(connection, Packets.Serverbound.HANDSHAKING_HANDSHAKE); buffer.writeVarInt((nextState == ConnectionState.STATUS ? -1 : connection.getVersion().getProtocolVersion())); // get best protocol version - buffer.writeString(address); - buffer.writeShort((short) port); + buffer.writeString(address.getHostname()); + buffer.writeShort((short) address.getPort()); buffer.writeVarInt(nextState.getId()); return buffer; } @Override public void log() { - Log.protocol(String.format("Sending handshake packet (host=%s, port=%d)", address, port)); + Log.protocol(String.format("Sending handshake packet (address=%s)", address)); } } diff --git a/src/main/java/de/bixilon/minosoft/protocol/protocol/ConnectionReason.java b/src/main/java/de/bixilon/minosoft/protocol/protocol/ConnectionReason.java index db4b1c643..e31c27f9a 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/protocol/ConnectionReason.java +++ b/src/main/java/de/bixilon/minosoft/protocol/protocol/ConnectionReason.java @@ -14,6 +14,7 @@ package de.bixilon.minosoft.protocol.protocol; public enum ConnectionReason { + DNS, PING, GET_VERSION, CONNECT diff --git a/src/main/java/de/bixilon/minosoft/protocol/protocol/ConnectionState.java b/src/main/java/de/bixilon/minosoft/protocol/protocol/ConnectionState.java index 828325e4a..7778cb66a 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/protocol/ConnectionState.java +++ b/src/main/java/de/bixilon/minosoft/protocol/protocol/ConnectionState.java @@ -20,7 +20,8 @@ public enum ConnectionState { LOGIN(2), PLAY(3), DISCONNECTING(5), - DISCONNECTED(6); + DISCONNECTED(6), + FAILED(7); final int id; diff --git a/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java b/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java index 17c321c07..51d0d6628 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java +++ b/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java @@ -16,6 +16,8 @@ package de.bixilon.minosoft.protocol.protocol; public final class ProtocolDefinition { public static final int STRING_MAX_LEN = 32767; public static final int DEFAULT_PORT = 25565; + public static final int SOCKET_CONNECT_TIMEOUT = 5000; + public static final int SOCKET_TIMEOUT = 30000; public static final int PROTOCOL_PACKET_MAX_SIZE = 2097152; public static final float ANGLE_CALCULATION_CONSTANT = 360.0F / 256.0F; diff --git a/src/main/java/de/bixilon/minosoft/util/DNSUtil.java b/src/main/java/de/bixilon/minosoft/util/DNSUtil.java new file mode 100644 index 000000000..ed90a65c0 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/DNSUtil.java @@ -0,0 +1,61 @@ +/* + * 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util; + +import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition; +import org.xbill.DNS.Record; +import org.xbill.DNS.*; + +import java.util.ArrayList; + +public class DNSUtil { + + public static ArrayList getServerAddresses(String hostname) throws TextParseException { + ServerAddress fallbackAddress = getServerAddress(hostname); + ArrayList ret = new ArrayList<>(); + if (hostname.contains(":")) { + // port provided, skip srv check + ret.add(fallbackAddress); + return ret; + } + String query = "_minecraft._tcp." + hostname; + Record[] records = new Lookup(query, Type.SRV).run(); + if (records == null) { + ret.add(fallbackAddress); + return ret; + } + for (Record record : records) { + SRVRecord srvRecord = (SRVRecord) record; + ret.add(new ServerAddress(srvRecord.getTarget().toString(true), srvRecord.getPort())); + } + ret.add(fallbackAddress); + return ret; + } + + private static ServerAddress getServerAddress(String hostname) { + String[] splitHostname = hostname.split(":", 2); + if (splitHostname.length == 1) { + return new ServerAddress(splitHostname[0], ProtocolDefinition.DEFAULT_PORT); + } + return new ServerAddress(splitHostname[0], Integer.parseInt(splitHostname[1])); + + } + + public String correctHostName(String hostname) { + // replaces invalid chars to avoid copy and paste issues (like spaces, ...) + hostname = hostname.replaceAll("\\s", ""); + return hostname; + } + +} diff --git a/src/main/java/de/bixilon/minosoft/util/ServerAddress.java b/src/main/java/de/bixilon/minosoft/util/ServerAddress.java new file mode 100644 index 000000000..5e7968c2a --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/util/ServerAddress.java @@ -0,0 +1,37 @@ +/* + * 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.util; + +public class ServerAddress { + final String hostname; + final int port; + + public ServerAddress(String hostname, int port) { + this.hostname = hostname; + this.port = port; + } + + public String getHostname() { + return hostname; + } + + public int getPort() { + return port; + } + + @Override + public String toString() { + return getHostname() + ":" + getPort(); + } +} diff --git a/src/main/resources/config/game.yml b/src/main/resources/config/game.yml index d3d6e9d5d..e657ebb87 100644 --- a/src/main/resources/config/game.yml +++ b/src/main/resources/config/game.yml @@ -17,6 +17,5 @@ account: # this will be removed soon, only for debugging (pre alpha stage): some features are not implemented yet -/- debug: - host: "127.0.0.1" - port: 25565 + server: "127.0.0.1" version: -1 \ No newline at end of file