From c689434ec4ef7e7df24942313f03271323dd4399 Mon Sep 17 00:00:00 2001 From: Rainnny7 Date: Mon, 22 Apr 2024 20:49:35 -0400 Subject: [PATCH] Add Java server querying! (: --- .../mc/common/packet/JavaQueryPacket.java | 101 +++++++++++++++ ...inecraftJavaPacket.java => TCPPacket.java} | 2 +- ...craftBedrockPacket.java => UDPPacket.java} | 4 +- ...java => BedrockUnconnectedPingPacket.java} | 4 +- ...java => BedrockUnconnectedPongPacket.java} | 6 +- .../JavaHandshakingInSetProtocolPacket.java} | 6 +- .../JavaStatusInStartPacket.java} | 6 +- .../udp/JavaQueryFullStatRequestPacket.java | 75 +++++++++++ .../udp/JavaQueryFullStatResponsePacket.java | 79 ++++++++++++ .../udp/JavaQueryHandshakeRequestPacket.java | 70 ++++++++++ .../udp/JavaQueryHandshakeResponsePacket.java | 64 ++++++++++ .../mc/model/server/JavaMinecraftServer.java | 74 +++++++++-- .../token/JavaServerChallengeStatusToken.java | 68 ++++++++++ .../me/braydon/mc/service/MojangService.java | 12 +- .../impl/BedrockMinecraftServerPinger.java | 12 +- .../impl/JavaMinecraftServerPinger.java | 120 ++++++++++++++---- 16 files changed, 640 insertions(+), 63 deletions(-) create mode 100644 API/src/main/java/me/braydon/mc/common/packet/JavaQueryPacket.java rename API/src/main/java/me/braydon/mc/common/packet/{MinecraftJavaPacket.java => TCPPacket.java} (98%) rename API/src/main/java/me/braydon/mc/common/packet/{MinecraftBedrockPacket.java => UDPPacket.java} (93%) rename API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/{BedrockPacketUnconnectedPing.java => BedrockUnconnectedPingPacket.java} (94%) rename API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/{BedrockPacketUnconnectedPong.java => BedrockUnconnectedPongPacket.java} (94%) rename API/src/main/java/me/braydon/mc/common/packet/impl/java/{JavaPacketHandshakingInSetProtocol.java => tcp/JavaHandshakingInSetProtocolPacket.java} (94%) rename API/src/main/java/me/braydon/mc/common/packet/impl/java/{JavaPacketStatusInStart.java => tcp/JavaStatusInStartPacket.java} (94%) create mode 100644 API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryFullStatRequestPacket.java create mode 100644 API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryFullStatResponsePacket.java create mode 100644 API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryHandshakeRequestPacket.java create mode 100644 API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryHandshakeResponsePacket.java create mode 100644 API/src/main/java/me/braydon/mc/model/token/JavaServerChallengeStatusToken.java diff --git a/API/src/main/java/me/braydon/mc/common/packet/JavaQueryPacket.java b/API/src/main/java/me/braydon/mc/common/packet/JavaQueryPacket.java new file mode 100644 index 0000000..bfb30f9 --- /dev/null +++ b/API/src/main/java/me/braydon/mc/common/packet/JavaQueryPacket.java @@ -0,0 +1,101 @@ +/* + * MIT License + * + * Copyright (c) 2024 Braydon (Rainnny). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package me.braydon.mc.common.packet; + +import java.util.ArrayList; + +/** + * @author Braydon + */ +public abstract class JavaQueryPacket extends UDPPacket { + protected static byte[] MAGIC = { (byte) 0xFE, (byte) 0xFD }; + + protected final byte[] padArrayEnd(byte[] array, int amount) { + byte[] result = new byte[array.length + amount]; + System.arraycopy(array, 0, result, 0, array.length); + for (int i = array.length; i < result.length; i++) { + result[i] = 0; + } + return result; + } + + protected final byte[] intToBytes(int input) { + return new byte[] { + (byte) (input >>> 24 & 0xFF), + (byte) (input >>> 16 & 0xFF), + (byte) (input >>> 8 & 0xFF), + (byte) (input & 0xFF) + }; + } + + protected final byte[] trim(byte[] arr) { + int begin = 0, end = arr.length; + for (int i = 0; i < arr.length; i++) { // find the first non-null byte{ + if (arr[i] != 0) { + begin = i; + break; + } + } + for (int i = arr.length - 1; i >= 0; i--) { //find the last non-null byte + if (arr[i] != 0) { + end = i; + break; + } + } + return subarray(arr, begin, end); + } + + protected final byte[] subarray(byte[] in, int a, int b) { + if (b - a > in.length) { + return in; + } + byte[] out = new byte[(b - a) + 1]; + if (b + 1 - a >= 0) { + System.arraycopy(in, a, out, 0, b + 1 - a); + } + return out; + } + + protected final byte[][] split(byte[] input) { + ArrayList temp = new ArrayList<>(); + int index_cache = 0; + for (int i = 0; i < input.length; i++) { + if (input[i] == 0x00) { + byte[] b = subarray(input, index_cache, i - 1); + temp.add(b); + index_cache = i + 1;//note, this is the index *after* the null byte + } + } + //get the remaining part + if (index_cache != 0) { //prevent duplication if there are no null bytes + byte[] b = subarray(input, index_cache, input.length - 1); + temp.add(b); + } + byte[][] output = new byte[temp.size()][input.length]; + for (int i = 0; i < temp.size(); i++) { + output[i] = temp.get(i); + } + return output; + } +} \ No newline at end of file diff --git a/API/src/main/java/me/braydon/mc/common/packet/MinecraftJavaPacket.java b/API/src/main/java/me/braydon/mc/common/packet/TCPPacket.java similarity index 98% rename from API/src/main/java/me/braydon/mc/common/packet/MinecraftJavaPacket.java rename to API/src/main/java/me/braydon/mc/common/packet/TCPPacket.java index 97f4468..4a00d2f 100644 --- a/API/src/main/java/me/braydon/mc/common/packet/MinecraftJavaPacket.java +++ b/API/src/main/java/me/braydon/mc/common/packet/TCPPacket.java @@ -36,7 +36,7 @@ import java.io.IOException; * @author Braydon * @see Protocol Docs */ -public abstract class MinecraftJavaPacket { +public abstract class TCPPacket { /** * Process this packet. * diff --git a/API/src/main/java/me/braydon/mc/common/packet/MinecraftBedrockPacket.java b/API/src/main/java/me/braydon/mc/common/packet/UDPPacket.java similarity index 93% rename from API/src/main/java/me/braydon/mc/common/packet/MinecraftBedrockPacket.java rename to API/src/main/java/me/braydon/mc/common/packet/UDPPacket.java index 23dbe8f..a8e54b9 100644 --- a/API/src/main/java/me/braydon/mc/common/packet/MinecraftBedrockPacket.java +++ b/API/src/main/java/me/braydon/mc/common/packet/UDPPacket.java @@ -35,12 +35,12 @@ import java.net.DatagramSocket; * @author Braydon * @see Protocol Docs */ -public interface MinecraftBedrockPacket { +public abstract class UDPPacket { /** * Process this packet. * * @param socket the socket to process the packet for * @throws IOException if an I/O error occurs */ - void process(@NonNull DatagramSocket socket) throws IOException; + public abstract void process(@NonNull DatagramSocket socket) throws IOException; } \ No newline at end of file diff --git a/API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/BedrockPacketUnconnectedPing.java b/API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/BedrockUnconnectedPingPacket.java similarity index 94% rename from API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/BedrockPacketUnconnectedPing.java rename to API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/BedrockUnconnectedPingPacket.java index d0d56e0..2bfafd1 100644 --- a/API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/BedrockPacketUnconnectedPing.java +++ b/API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/BedrockUnconnectedPingPacket.java @@ -24,7 +24,7 @@ package me.braydon.mc.common.packet.impl.bedrock; import lombok.NonNull; -import me.braydon.mc.common.packet.MinecraftBedrockPacket; +import me.braydon.mc.common.packet.UDPPacket; import java.io.IOException; import java.net.DatagramPacket; @@ -40,7 +40,7 @@ import java.nio.ByteOrder; * @author Braydon * @see Protocol Docs */ -public final class BedrockPacketUnconnectedPing implements MinecraftBedrockPacket { +public final class BedrockUnconnectedPingPacket extends UDPPacket { private static final byte ID = 0x01; // The ID of the packet private static final byte[] MAGIC = { 0, -1, -1, 0, -2, -2, -2, -2, -3, -3, -3, -3, 18, 52, 86, 120 }; diff --git a/API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/BedrockPacketUnconnectedPong.java b/API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/BedrockUnconnectedPongPacket.java similarity index 94% rename from API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/BedrockPacketUnconnectedPong.java rename to API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/BedrockUnconnectedPongPacket.java index 4ce86df..dacb3ad 100644 --- a/API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/BedrockPacketUnconnectedPong.java +++ b/API/src/main/java/me/braydon/mc/common/packet/impl/bedrock/BedrockUnconnectedPongPacket.java @@ -25,7 +25,7 @@ package me.braydon.mc.common.packet.impl.bedrock; import lombok.Getter; import lombok.NonNull; -import me.braydon.mc.common.packet.MinecraftBedrockPacket; +import me.braydon.mc.common.packet.UDPPacket; import me.braydon.mc.model.server.BedrockMinecraftServer; import java.io.IOException; @@ -37,13 +37,13 @@ import java.nio.charset.StandardCharsets; /** * This packet is sent by the server to the client in - * response to the {@link BedrockPacketUnconnectedPing}. + * response to the {@link BedrockUnconnectedPingPacket}. * * @author Braydon * @see Protocol Docs */ @Getter -public final class BedrockPacketUnconnectedPong implements MinecraftBedrockPacket { +public final class BedrockUnconnectedPongPacket extends UDPPacket { private static final byte ID = 0x1C; // The ID of the packet /** diff --git a/API/src/main/java/me/braydon/mc/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java b/API/src/main/java/me/braydon/mc/common/packet/impl/java/tcp/JavaHandshakingInSetProtocolPacket.java similarity index 94% rename from API/src/main/java/me/braydon/mc/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java rename to API/src/main/java/me/braydon/mc/common/packet/impl/java/tcp/JavaHandshakingInSetProtocolPacket.java index 945d460..0eaaa9a 100644 --- a/API/src/main/java/me/braydon/mc/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java +++ b/API/src/main/java/me/braydon/mc/common/packet/impl/java/tcp/JavaHandshakingInSetProtocolPacket.java @@ -21,12 +21,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package me.braydon.mc.common.packet.impl.java; +package me.braydon.mc.common.packet.impl.java.tcp; import lombok.AllArgsConstructor; import lombok.NonNull; import lombok.ToString; -import me.braydon.mc.common.packet.MinecraftJavaPacket; +import me.braydon.mc.common.packet.TCPPacket; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -41,7 +41,7 @@ import java.io.IOException; * @see Protocol Docs */ @AllArgsConstructor @ToString -public final class JavaPacketHandshakingInSetProtocol extends MinecraftJavaPacket { +public final class JavaHandshakingInSetProtocolPacket extends TCPPacket { private static final byte ID = 0x00; // The ID of the packet private static final int STATUS_HANDSHAKE = 1; // The status handshake ID diff --git a/API/src/main/java/me/braydon/mc/common/packet/impl/java/JavaPacketStatusInStart.java b/API/src/main/java/me/braydon/mc/common/packet/impl/java/tcp/JavaStatusInStartPacket.java similarity index 94% rename from API/src/main/java/me/braydon/mc/common/packet/impl/java/JavaPacketStatusInStart.java rename to API/src/main/java/me/braydon/mc/common/packet/impl/java/tcp/JavaStatusInStartPacket.java index 7e9009e..93197e8 100644 --- a/API/src/main/java/me/braydon/mc/common/packet/impl/java/JavaPacketStatusInStart.java +++ b/API/src/main/java/me/braydon/mc/common/packet/impl/java/tcp/JavaStatusInStartPacket.java @@ -21,11 +21,11 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package me.braydon.mc.common.packet.impl.java; +package me.braydon.mc.common.packet.impl.java.tcp; import lombok.Getter; import lombok.NonNull; -import me.braydon.mc.common.packet.MinecraftJavaPacket; +import me.braydon.mc.common.packet.TCPPacket; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -40,7 +40,7 @@ import java.io.IOException; * @see Protocol Docs */ @Getter -public final class JavaPacketStatusInStart extends MinecraftJavaPacket { +public final class JavaStatusInStartPacket extends TCPPacket { private static final byte ID = 0x00; // The ID of the packet /** diff --git a/API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryFullStatRequestPacket.java b/API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryFullStatRequestPacket.java new file mode 100644 index 0000000..8e5d337 --- /dev/null +++ b/API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryFullStatRequestPacket.java @@ -0,0 +1,75 @@ +/* + * MIT License + * + * Copyright (c) 2024 Braydon (Rainnny). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package me.braydon.mc.common.packet.impl.java.udp; + +import lombok.AllArgsConstructor; +import lombok.NonNull; +import me.braydon.mc.common.packet.JavaQueryPacket; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; + +/** + * This packet is sent by the client to the server to request the + * full stats of the server. The server will respond with a payload + * containing the server's stats. + * + * @author Braydon + * @see Query Protocol Docs + */ +@AllArgsConstructor +public final class JavaQueryFullStatRequestPacket extends JavaQueryPacket { + private static final int ID = 0; // The ID of the packet + + /** + * The response from the {@link JavaQueryHandshakeRequestPacket}. + */ + private final byte[] handshakeResponse; + + /** + * Process this packet. + * + * @param socket the socket to process the packet for + * @throws IOException if an I/O error occurs + */ + @Override + public void process(@NonNull DatagramSocket socket) throws IOException { + try ( + ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream(1460); + DataOutputStream dataOutputStream = new DataOutputStream(arrayOutputStream) + ) { + dataOutputStream.write(MAGIC); + dataOutputStream.write(ID); // Packet ID + dataOutputStream.writeInt(1); // Session ID + dataOutputStream.write(padArrayEnd(handshakeResponse, 4)); // The handshake response payload + + // Send the packet + byte[] bytes = arrayOutputStream.toByteArray(); + socket.send(new DatagramPacket(bytes, bytes.length)); + } + } +} \ No newline at end of file diff --git a/API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryFullStatResponsePacket.java b/API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryFullStatResponsePacket.java new file mode 100644 index 0000000..011498b --- /dev/null +++ b/API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryFullStatResponsePacket.java @@ -0,0 +1,79 @@ +/* + * MIT License + * + * Copyright (c) 2024 Braydon (Rainnny). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package me.braydon.mc.common.packet.impl.java.udp; + +import lombok.Getter; +import lombok.NonNull; +import me.braydon.mc.common.packet.JavaQueryPacket; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.util.HashMap; +import java.util.Map; + +/** + * This packet is sent by the server to the client in + * response to the {@link JavaQueryFullStatRequestPacket}. + * + * @author Braydon + * @see Query Protocol Docs + */ +@Getter +public final class JavaQueryFullStatResponsePacket extends JavaQueryPacket { + /** + * The response from the server, null if none. + */ + private Map response; + + /** + * Process this packet. + * + * @param socket the socket to process the packet for + * @throws IOException if an I/O error occurs + */ + @Override + public void process(@NonNull DatagramSocket socket) throws IOException { + // Handle receiving of the packet + byte[] receiveData = new byte[1024]; + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + socket.receive(receivePacket); + + // Construct a response from the received packet. + Map response = new HashMap<>(); + + String previousEntry = null; + for (byte[] bytes : split(trim(receivePacket.getData()))) { + String entry = new String(bytes); // The entry + + if (previousEntry != null) { + response.put(previousEntry, entry); + previousEntry = null; + continue; + } + previousEntry = entry; + } + this.response = response; + } +} \ No newline at end of file diff --git a/API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryHandshakeRequestPacket.java b/API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryHandshakeRequestPacket.java new file mode 100644 index 0000000..0929e32 --- /dev/null +++ b/API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryHandshakeRequestPacket.java @@ -0,0 +1,70 @@ +/* + * MIT License + * + * Copyright (c) 2024 Braydon (Rainnny). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package me.braydon.mc.common.packet.impl.java.udp; + +import lombok.NonNull; +import me.braydon.mc.common.packet.JavaQueryPacket; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; + +/** + * This packet is sent by the client to the + * server to request a handshake. This will + * then allow us to send further packets such + * as {@link JavaQueryFullStatRequestPacket}. + * + * @author Braydon + * @see Query Protocol Docs + */ +public final class JavaQueryHandshakeRequestPacket extends JavaQueryPacket { + private static final int ID = 9; // The ID of the packet + + /** + * Process this packet. + * + * @param socket the socket to process the packet for + * @throws IOException if an I/O error occurs + */ + @Override + public void process(@NonNull DatagramSocket socket) throws IOException { + try ( + ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream(1460); + DataOutputStream dataOutputStream = new DataOutputStream(arrayOutputStream) + ) { + dataOutputStream.write(MAGIC); + dataOutputStream.write(ID); // Packet ID + dataOutputStream.writeInt(1); // Session ID + dataOutputStream.write(new byte[] {}); // No payload data + + // Send the packet + byte[] bytes = arrayOutputStream.toByteArray(); + bytes = padArrayEnd(bytes, 11 - bytes.length); + socket.send(new DatagramPacket(bytes, bytes.length)); + } + } +} \ No newline at end of file diff --git a/API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryHandshakeResponsePacket.java b/API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryHandshakeResponsePacket.java new file mode 100644 index 0000000..e0ab5a4 --- /dev/null +++ b/API/src/main/java/me/braydon/mc/common/packet/impl/java/udp/JavaQueryHandshakeResponsePacket.java @@ -0,0 +1,64 @@ +/* + * MIT License + * + * Copyright (c) 2024 Braydon (Rainnny). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package me.braydon.mc.common.packet.impl.java.udp; + +import lombok.Getter; +import lombok.NonNull; +import me.braydon.mc.common.packet.JavaQueryPacket; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; + +/** + * This packet is sent by the server to the client in + * response to the {@link JavaQueryHandshakeRequestPacket}. + * + * @author Braydon + * @see Query Protocol Docs + */ +@Getter +public final class JavaQueryHandshakeResponsePacket extends JavaQueryPacket { + /** + * The response from the server. + */ + private byte[] response; + + /** + * Process this packet. + * + * @param socket the socket to process the packet for + * @throws IOException if an I/O error occurs + */ + @Override + public void process(@NonNull DatagramSocket socket) throws IOException { + // Handle receiving of the packet + byte[] receiveData = new byte[1024]; + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + socket.receive(receivePacket); + + // Set the response to the integer value of the received data + response = intToBytes(Integer.parseInt(new String(receivePacket.getData()).trim())); + } +} \ No newline at end of file diff --git a/API/src/main/java/me/braydon/mc/model/server/JavaMinecraftServer.java b/API/src/main/java/me/braydon/mc/model/server/JavaMinecraftServer.java index 074fbbc..49aaf9c 100644 --- a/API/src/main/java/me/braydon/mc/model/server/JavaMinecraftServer.java +++ b/API/src/main/java/me/braydon/mc/model/server/JavaMinecraftServer.java @@ -29,11 +29,16 @@ import me.braydon.mc.common.JavaMinecraftVersion; import me.braydon.mc.config.AppConfig; import me.braydon.mc.model.MinecraftServer; import me.braydon.mc.model.dns.DNSRecord; +import me.braydon.mc.model.token.JavaServerChallengeStatusToken; import me.braydon.mc.model.token.JavaServerStatusToken; import me.braydon.mc.service.MojangService; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.chat.ComponentSerializer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + /** * A Java edition {@link MinecraftServer}. * @@ -51,6 +56,12 @@ public final class JavaMinecraftServer extends MinecraftServer { */ private final Favicon favicon; + /** + * The plugins on this server, present if + * query is on and plugins are present. + */ + private final Plugin[] plugins; + /** * The Forge mod information for this server, null if none. *

@@ -67,6 +78,16 @@ public final class JavaMinecraftServer extends MinecraftServer { */ private final ForgeData forgeData; + /** + * The main world of this server, present if query is on. + */ + private final String world; + + /** + * Does this server support querying? + */ + private final boolean queryEnabled; + /** * Does this server preview chat? * @@ -98,13 +119,16 @@ public final class JavaMinecraftServer extends MinecraftServer { private boolean mojangBanned; private JavaMinecraftServer(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records, @NonNull Version version, - @NonNull Players players, @NonNull MOTD motd, Favicon favicon, ModInfo modInfo, ForgeData forgeData, - boolean previewsChat, boolean enforcesSecureChat, boolean preventsChatReports, boolean mojangBanned) { + @NonNull Players players, @NonNull MOTD motd, Favicon favicon, Plugin[] plugins, ModInfo modInfo, ForgeData forgeData, + String world, boolean queryEnabled, boolean previewsChat, boolean enforcesSecureChat, boolean preventsChatReports, boolean mojangBanned) { super(hostname, ip, port, records, players, motd); this.version = version; this.favicon = favicon; + this.plugins = plugins; this.modInfo = modInfo; this.forgeData = forgeData; + this.world = world; + this.queryEnabled = queryEnabled; this.previewsChat = previewsChat; this.enforcesSecureChat = enforcesSecureChat; this.preventsChatReports = preventsChatReports; @@ -118,19 +142,33 @@ public final class JavaMinecraftServer extends MinecraftServer { * @param ip the IP address of the server * @param port the port of the server * @param records the DNS records of the server - * @param token the status token + * @param statusToken the status token + * @param challengeStatusToken the challenge status token, null if none * @return the Java Minecraft server */ @NonNull - public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, - @NonNull DNSRecord[] records, @NonNull JavaServerStatusToken token) { - String motdString = token.getDescription() instanceof String ? (String) token.getDescription() : null; + public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records, + @NonNull JavaServerStatusToken statusToken, JavaServerChallengeStatusToken challengeStatusToken) { + String motdString = statusToken.getDescription() instanceof String ? (String) statusToken.getDescription() : null; if (motdString == null) { // Not a string motd, convert from Json - motdString = new TextComponent(ComponentSerializer.parse(AppConfig.GSON.toJson(token.getDescription()))).toLegacyText(); + motdString = new TextComponent(ComponentSerializer.parse(AppConfig.GSON.toJson(statusToken.getDescription()))).toLegacyText(); } - return new JavaMinecraftServer(hostname, ip, port, records, token.getVersion().detailedCopy(), Players.create(token.getPlayers()), - MOTD.create(motdString), Favicon.create(token.getFavicon(), hostname), token.getModInfo(), token.getForgeData(), - token.isPreviewsChat(), token.isEnforcesSecureChat(), token.isPreventsChatReports(), false + + // Get the plugins from the challenge token + Plugin[] plugins = null; + if (challengeStatusToken != null) { + List list = new ArrayList<>(); + for (Map.Entry entry : challengeStatusToken.getPlugins().entrySet()) { + list.add(new Plugin(entry.getKey(), entry.getValue())); + } + plugins = list.toArray(new Plugin[0]); + } + String world = challengeStatusToken == null ? null : challengeStatusToken.getMap(); + + return new JavaMinecraftServer(hostname, ip, port, records, statusToken.getVersion().detailedCopy(), Players.create(statusToken.getPlayers()), + MOTD.create(motdString), Favicon.create(statusToken.getFavicon(), hostname), plugins, statusToken.getModInfo(), statusToken.getForgeData(), + world, challengeStatusToken != null, statusToken.isPreviewsChat(), statusToken.isEnforcesSecureChat(), + statusToken.isPreventsChatReports(), false ); } @@ -220,6 +258,22 @@ public final class JavaMinecraftServer extends MinecraftServer { } } + /** + * A plugin for a server. + */ + @AllArgsConstructor @Getter @ToString + public static class Plugin { + /** + * The name of this plugin. + */ + @NonNull private final String name; + + /** + * The version of this plugin. + */ + @NonNull private final String version; + } + /** * Forge mod information for a server. *

diff --git a/API/src/main/java/me/braydon/mc/model/token/JavaServerChallengeStatusToken.java b/API/src/main/java/me/braydon/mc/model/token/JavaServerChallengeStatusToken.java new file mode 100644 index 0000000..26b148c --- /dev/null +++ b/API/src/main/java/me/braydon/mc/model/token/JavaServerChallengeStatusToken.java @@ -0,0 +1,68 @@ +/* + * MIT License + * + * Copyright (c) 2024 Braydon (Rainnny). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package me.braydon.mc.model.token; + +import lombok.*; +import me.braydon.mc.model.server.JavaMinecraftServer; + +import java.util.HashMap; +import java.util.Map; + +/** + * A token representing the response from + * sending a challenge request via UDP to + * a {@link JavaMinecraftServer} using the + * query. + * + * @author Braydon + */ +@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @ToString +public final class JavaServerChallengeStatusToken { + /** + * The map (world) of this server. + */ + @NonNull private final String map; + + /** + * The plugins of this server. + */ + private final Map plugins; + + /** + * Create a new challenge token + * from the given raw data. + * + * @param rawData the raw data + * @return the challenge token + */ + @NonNull + public static JavaServerChallengeStatusToken create(@NonNull Map rawData) { + Map plugins = new HashMap<>(); + for (String plugin : rawData.get("plugins").split(": ")[1].split("; ")) { + String[] split = plugin.split(" "); + plugins.put(split[0], split[1]); + } + return new JavaServerChallengeStatusToken(rawData.get("map"), plugins); + } +} \ No newline at end of file diff --git a/API/src/main/java/me/braydon/mc/service/MojangService.java b/API/src/main/java/me/braydon/mc/service/MojangService.java index 1a3b830..48bf3b3 100644 --- a/API/src/main/java/me/braydon/mc/service/MojangService.java +++ b/API/src/main/java/me/braydon/mc/service/MojangService.java @@ -431,11 +431,11 @@ public final class MojangService { } // Check the cache for the server - Optional cached = minecraftServerCache.findById("%s-%s".formatted(platform.name(), lookupHostname)); - if (cached.isPresent()) { // Respond with the cache if present - log.info("Found server in cache: {}", hostname); - return cached.get(); - } +// Optional cached = minecraftServerCache.findById("%s-%s".formatted(platform.name(), lookupHostname)); +// if (cached.isPresent()) { // Respond with the cache if present +// log.info("Found server in cache: {}", hostname); +// return cached.get(); +// } List records = new ArrayList<>(); // The resolved DNS records for the server SRVRecord srvRecord = platform == MinecraftServer.Platform.JAVA ? DNSUtils.resolveSRV(hostname) : null; // Resolve the SRV record @@ -467,7 +467,7 @@ public final class MojangService { ((JavaMinecraftServer) minecraftServer.getValue()).setMojangBanned(isServerBlocked(hostname)); } - minecraftServerCache.save(minecraftServer); +// minecraftServerCache.save(minecraftServer); log.info("Cached server: {}", hostname); minecraftServer.setCached(-1L); // Set to -1 to indicate it's not cached in the response return minecraftServer; diff --git a/API/src/main/java/me/braydon/mc/service/pinger/impl/BedrockMinecraftServerPinger.java b/API/src/main/java/me/braydon/mc/service/pinger/impl/BedrockMinecraftServerPinger.java index cdd8ec2..60a97b7 100644 --- a/API/src/main/java/me/braydon/mc/service/pinger/impl/BedrockMinecraftServerPinger.java +++ b/API/src/main/java/me/braydon/mc/service/pinger/impl/BedrockMinecraftServerPinger.java @@ -25,8 +25,8 @@ package me.braydon.mc.service.pinger.impl; import lombok.NonNull; import lombok.extern.log4j.Log4j2; -import me.braydon.mc.common.packet.impl.bedrock.BedrockPacketUnconnectedPing; -import me.braydon.mc.common.packet.impl.bedrock.BedrockPacketUnconnectedPong; +import me.braydon.mc.common.packet.impl.bedrock.BedrockUnconnectedPingPacket; +import me.braydon.mc.common.packet.impl.bedrock.BedrockUnconnectedPongPacket; import me.braydon.mc.exception.impl.BadRequestException; import me.braydon.mc.exception.impl.ResourceNotFoundException; import me.braydon.mc.model.dns.DNSRecord; @@ -60,7 +60,7 @@ public final class BedrockMinecraftServerPinger implements MinecraftServerPinger */ @Override public BedrockMinecraftServer ping(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records) { - log.info("Pinging {}:{}...", hostname, port); + log.info("Opening UDP connection to {}:{}...", hostname, port); long before = System.currentTimeMillis(); // Timestamp before pinging // Open a socket connection to the server @@ -69,13 +69,13 @@ public final class BedrockMinecraftServerPinger implements MinecraftServerPinger socket.connect(new InetSocketAddress(hostname, port)); long ping = System.currentTimeMillis() - before; // Calculate the ping - log.info("Pinged {}:{} in {}ms", hostname, port, ping); + log.info("UDP Connection to {}:{} opened. Ping: {}ms", hostname, port, ping); // Send the unconnected ping packet - new BedrockPacketUnconnectedPing().process(socket); + new BedrockUnconnectedPingPacket().process(socket); // Handle the received unconnected pong packet - BedrockPacketUnconnectedPong unconnectedPong = new BedrockPacketUnconnectedPong(); + BedrockUnconnectedPongPacket unconnectedPong = new BedrockUnconnectedPongPacket(); unconnectedPong.process(socket); String response = unconnectedPong.getResponse(); if (response == null) { // No pong response diff --git a/API/src/main/java/me/braydon/mc/service/pinger/impl/JavaMinecraftServerPinger.java b/API/src/main/java/me/braydon/mc/service/pinger/impl/JavaMinecraftServerPinger.java index 0e84ae0..df093b4 100644 --- a/API/src/main/java/me/braydon/mc/service/pinger/impl/JavaMinecraftServerPinger.java +++ b/API/src/main/java/me/braydon/mc/service/pinger/impl/JavaMinecraftServerPinger.java @@ -26,13 +26,18 @@ package me.braydon.mc.service.pinger.impl; import lombok.NonNull; import lombok.extern.log4j.Log4j2; import me.braydon.mc.common.JavaMinecraftVersion; -import me.braydon.mc.common.packet.impl.java.JavaPacketHandshakingInSetProtocol; -import me.braydon.mc.common.packet.impl.java.JavaPacketStatusInStart; +import me.braydon.mc.common.packet.impl.java.tcp.JavaHandshakingInSetProtocolPacket; +import me.braydon.mc.common.packet.impl.java.tcp.JavaStatusInStartPacket; +import me.braydon.mc.common.packet.impl.java.udp.JavaQueryFullStatRequestPacket; +import me.braydon.mc.common.packet.impl.java.udp.JavaQueryFullStatResponsePacket; +import me.braydon.mc.common.packet.impl.java.udp.JavaQueryHandshakeRequestPacket; +import me.braydon.mc.common.packet.impl.java.udp.JavaQueryHandshakeResponsePacket; import me.braydon.mc.config.AppConfig; import me.braydon.mc.exception.impl.BadRequestException; import me.braydon.mc.exception.impl.ResourceNotFoundException; import me.braydon.mc.model.dns.DNSRecord; import me.braydon.mc.model.server.JavaMinecraftServer; +import me.braydon.mc.model.token.JavaServerChallengeStatusToken; import me.braydon.mc.model.token.JavaServerStatusToken; import me.braydon.mc.service.pinger.MinecraftServerPinger; @@ -43,7 +48,7 @@ import java.net.*; /** * The {@link MinecraftServerPinger} for pinging - * {@link JavaMinecraftServer}'s over TCP. + * {@link JavaMinecraftServer}'s over TCP/UDP. * * @author Braydon */ @@ -63,33 +68,25 @@ public final class JavaMinecraftServerPinger implements MinecraftServerPinger