Add Java server querying! (:
Some checks failed
Deploy API / docker (17, 3.8.5) (push) Has been cancelled
Some checks failed
Deploy API / docker (17, 3.8.5) (push) Has been cancelled
This commit is contained in:
parent
345e1532a4
commit
c689434ec4
@ -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<byte[]> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -36,7 +36,7 @@ import java.io.IOException;
|
|||||||
* @author Braydon
|
* @author Braydon
|
||||||
* @see <a href="https://wiki.vg/Protocol">Protocol Docs</a>
|
* @see <a href="https://wiki.vg/Protocol">Protocol Docs</a>
|
||||||
*/
|
*/
|
||||||
public abstract class MinecraftJavaPacket {
|
public abstract class TCPPacket {
|
||||||
/**
|
/**
|
||||||
* Process this packet.
|
* Process this packet.
|
||||||
*
|
*
|
@ -35,12 +35,12 @@ import java.net.DatagramSocket;
|
|||||||
* @author Braydon
|
* @author Braydon
|
||||||
* @see <a href="https://wiki.vg/Raknet_Protocol">Protocol Docs</a>
|
* @see <a href="https://wiki.vg/Raknet_Protocol">Protocol Docs</a>
|
||||||
*/
|
*/
|
||||||
public interface MinecraftBedrockPacket {
|
public abstract class UDPPacket {
|
||||||
/**
|
/**
|
||||||
* Process this packet.
|
* Process this packet.
|
||||||
*
|
*
|
||||||
* @param socket the socket to process the packet for
|
* @param socket the socket to process the packet for
|
||||||
* @throws IOException if an I/O error occurs
|
* @throws IOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
void process(@NonNull DatagramSocket socket) throws IOException;
|
public abstract void process(@NonNull DatagramSocket socket) throws IOException;
|
||||||
}
|
}
|
@ -24,7 +24,7 @@
|
|||||||
package me.braydon.mc.common.packet.impl.bedrock;
|
package me.braydon.mc.common.packet.impl.bedrock;
|
||||||
|
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import me.braydon.mc.common.packet.MinecraftBedrockPacket;
|
import me.braydon.mc.common.packet.UDPPacket;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.DatagramPacket;
|
import java.net.DatagramPacket;
|
||||||
@ -40,7 +40,7 @@ import java.nio.ByteOrder;
|
|||||||
* @author Braydon
|
* @author Braydon
|
||||||
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Ping">Protocol Docs</a>
|
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Ping">Protocol Docs</a>
|
||||||
*/
|
*/
|
||||||
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 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 };
|
private static final byte[] MAGIC = { 0, -1, -1, 0, -2, -2, -2, -2, -3, -3, -3, -3, 18, 52, 86, 120 };
|
||||||
|
|
@ -25,7 +25,7 @@ package me.braydon.mc.common.packet.impl.bedrock;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
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 me.braydon.mc.model.server.BedrockMinecraftServer;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -37,13 +37,13 @@ import java.nio.charset.StandardCharsets;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This packet is sent by the server to the client in
|
* This packet is sent by the server to the client in
|
||||||
* response to the {@link BedrockPacketUnconnectedPing}.
|
* response to the {@link BedrockUnconnectedPingPacket}.
|
||||||
*
|
*
|
||||||
* @author Braydon
|
* @author Braydon
|
||||||
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Pong">Protocol Docs</a>
|
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Pong">Protocol Docs</a>
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
public final class BedrockPacketUnconnectedPong implements MinecraftBedrockPacket {
|
public final class BedrockUnconnectedPongPacket extends UDPPacket {
|
||||||
private static final byte ID = 0x1C; // The ID of the packet
|
private static final byte ID = 0x1C; // The ID of the packet
|
||||||
|
|
||||||
/**
|
/**
|
@ -21,12 +21,12 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package me.braydon.mc.common.packet.impl.java;
|
package me.braydon.mc.common.packet.impl.java.tcp;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import me.braydon.mc.common.packet.MinecraftJavaPacket;
|
import me.braydon.mc.common.packet.TCPPacket;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
@ -41,7 +41,7 @@ import java.io.IOException;
|
|||||||
* @see <a href="https://wiki.vg/Protocol#Handshake">Protocol Docs</a>
|
* @see <a href="https://wiki.vg/Protocol#Handshake">Protocol Docs</a>
|
||||||
*/
|
*/
|
||||||
@AllArgsConstructor @ToString
|
@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 byte ID = 0x00; // The ID of the packet
|
||||||
private static final int STATUS_HANDSHAKE = 1; // The status handshake ID
|
private static final int STATUS_HANDSHAKE = 1; // The status handshake ID
|
||||||
|
|
@ -21,11 +21,11 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package me.braydon.mc.common.packet.impl.java;
|
package me.braydon.mc.common.packet.impl.java.tcp;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import me.braydon.mc.common.packet.MinecraftJavaPacket;
|
import me.braydon.mc.common.packet.TCPPacket;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
@ -40,7 +40,7 @@ import java.io.IOException;
|
|||||||
* @see <a href="https://wiki.vg/Protocol#Status_Request">Protocol Docs</a>
|
* @see <a href="https://wiki.vg/Protocol#Status_Request">Protocol Docs</a>
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
public final class JavaPacketStatusInStart extends MinecraftJavaPacket {
|
public final class JavaStatusInStartPacket extends TCPPacket {
|
||||||
private static final byte ID = 0x00; // The ID of the packet
|
private static final byte ID = 0x00; // The ID of the packet
|
||||||
|
|
||||||
/**
|
/**
|
@ -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 <a href="https://wiki.vg/Query#Request_3">Query Protocol Docs</a>
|
||||||
|
*/
|
||||||
|
@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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 <a href="https://wiki.vg/Query#Response_3">Query Protocol Docs</a>
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public final class JavaQueryFullStatResponsePacket extends JavaQueryPacket {
|
||||||
|
/**
|
||||||
|
* The response from the server, null if none.
|
||||||
|
*/
|
||||||
|
private Map<String, String> 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<String, String> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 <a href="https://wiki.vg/Query#Request">Query Protocol Docs</a>
|
||||||
|
*/
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 <a href="https://wiki.vg/Query#Response">Query Protocol Docs</a>
|
||||||
|
*/
|
||||||
|
@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()));
|
||||||
|
}
|
||||||
|
}
|
@ -29,11 +29,16 @@ import me.braydon.mc.common.JavaMinecraftVersion;
|
|||||||
import me.braydon.mc.config.AppConfig;
|
import me.braydon.mc.config.AppConfig;
|
||||||
import me.braydon.mc.model.MinecraftServer;
|
import me.braydon.mc.model.MinecraftServer;
|
||||||
import me.braydon.mc.model.dns.DNSRecord;
|
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.model.token.JavaServerStatusToken;
|
||||||
import me.braydon.mc.service.MojangService;
|
import me.braydon.mc.service.MojangService;
|
||||||
import net.md_5.bungee.api.chat.TextComponent;
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
import net.md_5.bungee.chat.ComponentSerializer;
|
import net.md_5.bungee.chat.ComponentSerializer;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Java edition {@link MinecraftServer}.
|
* A Java edition {@link MinecraftServer}.
|
||||||
*
|
*
|
||||||
@ -51,6 +56,12 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
|||||||
*/
|
*/
|
||||||
private final Favicon favicon;
|
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.
|
* The Forge mod information for this server, null if none.
|
||||||
* <p>
|
* <p>
|
||||||
@ -67,6 +78,16 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
|||||||
*/
|
*/
|
||||||
private final ForgeData forgeData;
|
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?
|
* Does this server preview chat?
|
||||||
*
|
*
|
||||||
@ -98,13 +119,16 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
|||||||
private boolean mojangBanned;
|
private boolean mojangBanned;
|
||||||
|
|
||||||
private JavaMinecraftServer(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records, @NonNull Version version,
|
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,
|
@NonNull Players players, @NonNull MOTD motd, Favicon favicon, Plugin[] plugins, ModInfo modInfo, ForgeData forgeData,
|
||||||
boolean previewsChat, boolean enforcesSecureChat, boolean preventsChatReports, boolean mojangBanned) {
|
String world, boolean queryEnabled, boolean previewsChat, boolean enforcesSecureChat, boolean preventsChatReports, boolean mojangBanned) {
|
||||||
super(hostname, ip, port, records, players, motd);
|
super(hostname, ip, port, records, players, motd);
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.favicon = favicon;
|
this.favicon = favicon;
|
||||||
|
this.plugins = plugins;
|
||||||
this.modInfo = modInfo;
|
this.modInfo = modInfo;
|
||||||
this.forgeData = forgeData;
|
this.forgeData = forgeData;
|
||||||
|
this.world = world;
|
||||||
|
this.queryEnabled = queryEnabled;
|
||||||
this.previewsChat = previewsChat;
|
this.previewsChat = previewsChat;
|
||||||
this.enforcesSecureChat = enforcesSecureChat;
|
this.enforcesSecureChat = enforcesSecureChat;
|
||||||
this.preventsChatReports = preventsChatReports;
|
this.preventsChatReports = preventsChatReports;
|
||||||
@ -118,19 +142,33 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
|||||||
* @param ip the IP address of the server
|
* @param ip the IP address of the server
|
||||||
* @param port the port of the server
|
* @param port the port of the server
|
||||||
* @param records the DNS records 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
|
* @return the Java Minecraft server
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port,
|
public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records,
|
||||||
@NonNull DNSRecord[] records, @NonNull JavaServerStatusToken token) {
|
@NonNull JavaServerStatusToken statusToken, JavaServerChallengeStatusToken challengeStatusToken) {
|
||||||
String motdString = token.getDescription() instanceof String ? (String) token.getDescription() : null;
|
String motdString = statusToken.getDescription() instanceof String ? (String) statusToken.getDescription() : null;
|
||||||
if (motdString == null) { // Not a string motd, convert from Json
|
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(),
|
// Get the plugins from the challenge token
|
||||||
token.isPreviewsChat(), token.isEnforcesSecureChat(), token.isPreventsChatReports(), false
|
Plugin[] plugins = null;
|
||||||
|
if (challengeStatusToken != null) {
|
||||||
|
List<Plugin> list = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, String> 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.
|
* Forge mod information for a server.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -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<String, String> 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<String, String> rawData) {
|
||||||
|
Map<String, String> 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);
|
||||||
|
}
|
||||||
|
}
|
@ -431,11 +431,11 @@ public final class MojangService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check the cache for the server
|
// Check the cache for the server
|
||||||
Optional<CachedMinecraftServer> cached = minecraftServerCache.findById("%s-%s".formatted(platform.name(), lookupHostname));
|
// Optional<CachedMinecraftServer> cached = minecraftServerCache.findById("%s-%s".formatted(platform.name(), lookupHostname));
|
||||||
if (cached.isPresent()) { // Respond with the cache if present
|
// if (cached.isPresent()) { // Respond with the cache if present
|
||||||
log.info("Found server in cache: {}", hostname);
|
// log.info("Found server in cache: {}", hostname);
|
||||||
return cached.get();
|
// return cached.get();
|
||||||
}
|
// }
|
||||||
List<DNSRecord> records = new ArrayList<>(); // The resolved DNS records for the server
|
List<DNSRecord> records = new ArrayList<>(); // The resolved DNS records for the server
|
||||||
|
|
||||||
SRVRecord srvRecord = platform == MinecraftServer.Platform.JAVA ? DNSUtils.resolveSRV(hostname) : null; // Resolve the SRV record
|
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));
|
((JavaMinecraftServer) minecraftServer.getValue()).setMojangBanned(isServerBlocked(hostname));
|
||||||
}
|
}
|
||||||
|
|
||||||
minecraftServerCache.save(minecraftServer);
|
// minecraftServerCache.save(minecraftServer);
|
||||||
log.info("Cached server: {}", hostname);
|
log.info("Cached server: {}", hostname);
|
||||||
minecraftServer.setCached(-1L); // Set to -1 to indicate it's not cached in the response
|
minecraftServer.setCached(-1L); // Set to -1 to indicate it's not cached in the response
|
||||||
return minecraftServer;
|
return minecraftServer;
|
||||||
|
@ -25,8 +25,8 @@ package me.braydon.mc.service.pinger.impl;
|
|||||||
|
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import me.braydon.mc.common.packet.impl.bedrock.BedrockPacketUnconnectedPing;
|
import me.braydon.mc.common.packet.impl.bedrock.BedrockUnconnectedPingPacket;
|
||||||
import me.braydon.mc.common.packet.impl.bedrock.BedrockPacketUnconnectedPong;
|
import me.braydon.mc.common.packet.impl.bedrock.BedrockUnconnectedPongPacket;
|
||||||
import me.braydon.mc.exception.impl.BadRequestException;
|
import me.braydon.mc.exception.impl.BadRequestException;
|
||||||
import me.braydon.mc.exception.impl.ResourceNotFoundException;
|
import me.braydon.mc.exception.impl.ResourceNotFoundException;
|
||||||
import me.braydon.mc.model.dns.DNSRecord;
|
import me.braydon.mc.model.dns.DNSRecord;
|
||||||
@ -60,7 +60,7 @@ public final class BedrockMinecraftServerPinger implements MinecraftServerPinger
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public BedrockMinecraftServer ping(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records) {
|
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
|
long before = System.currentTimeMillis(); // Timestamp before pinging
|
||||||
|
|
||||||
// Open a socket connection to the server
|
// Open a socket connection to the server
|
||||||
@ -69,13 +69,13 @@ public final class BedrockMinecraftServerPinger implements MinecraftServerPinger
|
|||||||
socket.connect(new InetSocketAddress(hostname, port));
|
socket.connect(new InetSocketAddress(hostname, port));
|
||||||
|
|
||||||
long ping = System.currentTimeMillis() - before; // Calculate the ping
|
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
|
// Send the unconnected ping packet
|
||||||
new BedrockPacketUnconnectedPing().process(socket);
|
new BedrockUnconnectedPingPacket().process(socket);
|
||||||
|
|
||||||
// Handle the received unconnected pong packet
|
// Handle the received unconnected pong packet
|
||||||
BedrockPacketUnconnectedPong unconnectedPong = new BedrockPacketUnconnectedPong();
|
BedrockUnconnectedPongPacket unconnectedPong = new BedrockUnconnectedPongPacket();
|
||||||
unconnectedPong.process(socket);
|
unconnectedPong.process(socket);
|
||||||
String response = unconnectedPong.getResponse();
|
String response = unconnectedPong.getResponse();
|
||||||
if (response == null) { // No pong response
|
if (response == null) { // No pong response
|
||||||
|
@ -26,13 +26,18 @@ package me.braydon.mc.service.pinger.impl;
|
|||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import me.braydon.mc.common.JavaMinecraftVersion;
|
import me.braydon.mc.common.JavaMinecraftVersion;
|
||||||
import me.braydon.mc.common.packet.impl.java.JavaPacketHandshakingInSetProtocol;
|
import me.braydon.mc.common.packet.impl.java.tcp.JavaHandshakingInSetProtocolPacket;
|
||||||
import me.braydon.mc.common.packet.impl.java.JavaPacketStatusInStart;
|
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.config.AppConfig;
|
||||||
import me.braydon.mc.exception.impl.BadRequestException;
|
import me.braydon.mc.exception.impl.BadRequestException;
|
||||||
import me.braydon.mc.exception.impl.ResourceNotFoundException;
|
import me.braydon.mc.exception.impl.ResourceNotFoundException;
|
||||||
import me.braydon.mc.model.dns.DNSRecord;
|
import me.braydon.mc.model.dns.DNSRecord;
|
||||||
import me.braydon.mc.model.server.JavaMinecraftServer;
|
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.model.token.JavaServerStatusToken;
|
||||||
import me.braydon.mc.service.pinger.MinecraftServerPinger;
|
import me.braydon.mc.service.pinger.MinecraftServerPinger;
|
||||||
|
|
||||||
@ -43,7 +48,7 @@ import java.net.*;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link MinecraftServerPinger} for pinging
|
* The {@link MinecraftServerPinger} for pinging
|
||||||
* {@link JavaMinecraftServer}'s over TCP.
|
* {@link JavaMinecraftServer}'s over TCP/UDP.
|
||||||
*
|
*
|
||||||
* @author Braydon
|
* @author Braydon
|
||||||
*/
|
*/
|
||||||
@ -63,33 +68,25 @@ public final class JavaMinecraftServerPinger implements MinecraftServerPinger<Ja
|
|||||||
@Override
|
@Override
|
||||||
public JavaMinecraftServer ping(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records) {
|
public JavaMinecraftServer ping(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records) {
|
||||||
log.info("Pinging {}:{}...", hostname, port);
|
log.info("Pinging {}:{}...", hostname, port);
|
||||||
long before = System.currentTimeMillis(); // Timestamp before pinging
|
|
||||||
|
|
||||||
// Open a socket connection to the server
|
try {
|
||||||
try (Socket socket = new Socket()) {
|
// Ping the server and retrieve both the status token, and the challenge status token
|
||||||
socket.setTcpNoDelay(true);
|
JavaServerStatusToken statusToken = retrieveStatusToken(hostname, port);
|
||||||
socket.connect(new InetSocketAddress(hostname, port), TIMEOUT);
|
JavaServerChallengeStatusToken challengeStatusToken = null;
|
||||||
|
try {
|
||||||
long ping = System.currentTimeMillis() - before; // Calculate the ping
|
challengeStatusToken = retrieveChallengeStatusToken(hostname, port);
|
||||||
log.info("Pinged {}:{} in {}ms", hostname, port, ping);
|
} catch (Exception ex) {
|
||||||
|
// An exception will be raised if querying
|
||||||
// Open data streams to begin packet transaction
|
// is disabled on the server. If the exception
|
||||||
try (DataInputStream inputStream = new DataInputStream(socket.getInputStream());
|
// is not caused by querying being disabled, we
|
||||||
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream())) {
|
// want to log the error.
|
||||||
// Begin handshaking with the server
|
if (!(ex instanceof IOException)) {
|
||||||
new JavaPacketHandshakingInSetProtocol(hostname, port, JavaMinecraftVersion.getMinimumVersion().getProtocol())
|
log.error("Failed retrieving challenge status token for %s:%s:".formatted(hostname, port), ex);
|
||||||
.process(inputStream, outputStream);
|
|
||||||
|
|
||||||
// Send the status request to the server, and await back the response
|
|
||||||
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
|
|
||||||
packetStatusInStart.process(inputStream, outputStream);
|
|
||||||
String response = packetStatusInStart.getResponse();
|
|
||||||
if (response == null) { // No response
|
|
||||||
throw new ResourceNotFoundException("Server didn't respond to status request");
|
|
||||||
}
|
}
|
||||||
JavaServerStatusToken token = AppConfig.GSON.fromJson(response, JavaServerStatusToken.class);
|
|
||||||
return JavaMinecraftServer.create(hostname, ip, port, records, token); // Return the server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the server
|
||||||
|
return JavaMinecraftServer.create(hostname, ip, port, records, statusToken, challengeStatusToken);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
if (ex instanceof UnknownHostException) {
|
if (ex instanceof UnknownHostException) {
|
||||||
throw new BadRequestException("Unknown hostname: %s".formatted(hostname));
|
throw new BadRequestException("Unknown hostname: %s".formatted(hostname));
|
||||||
@ -100,4 +97,73 @@ public final class JavaMinecraftServerPinger implements MinecraftServerPinger<Ja
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ping a server and retrieve its response.
|
||||||
|
*
|
||||||
|
* @param hostname the hostname to ping
|
||||||
|
* @param port the port to ping
|
||||||
|
* @return the status token
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
* @throws ResourceNotFoundException if the server didn't respond
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private JavaServerStatusToken retrieveStatusToken(@NonNull String hostname, int port) throws IOException, ResourceNotFoundException {
|
||||||
|
log.info("Opening TCP connection to {}:{}...", hostname, port);
|
||||||
|
long before = System.currentTimeMillis(); // Timestamp before pinging
|
||||||
|
|
||||||
|
// Open a socket connection to the server
|
||||||
|
try (Socket socket = new Socket()) {
|
||||||
|
socket.setTcpNoDelay(true);
|
||||||
|
socket.connect(new InetSocketAddress(hostname, port), TIMEOUT);
|
||||||
|
|
||||||
|
long ping = System.currentTimeMillis() - before; // Calculate the ping
|
||||||
|
log.info("TCP Connection to {}:{} opened. Ping: {}ms", hostname, port, ping);
|
||||||
|
|
||||||
|
// Begin packet transaction
|
||||||
|
try (DataInputStream inputStream = new DataInputStream(socket.getInputStream());
|
||||||
|
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream())) {
|
||||||
|
// Begin handshaking with the server
|
||||||
|
new JavaHandshakingInSetProtocolPacket(hostname, port, JavaMinecraftVersion.getMinimumVersion().getProtocol())
|
||||||
|
.process(inputStream, outputStream);
|
||||||
|
|
||||||
|
// Send the status request to the server, and await back the response
|
||||||
|
JavaStatusInStartPacket packetStatusInStart = new JavaStatusInStartPacket();
|
||||||
|
packetStatusInStart.process(inputStream, outputStream);
|
||||||
|
String response = packetStatusInStart.getResponse();
|
||||||
|
if (response == null) { // No response
|
||||||
|
throw new ResourceNotFoundException("Server didn't respond to status request");
|
||||||
|
}
|
||||||
|
return AppConfig.GSON.fromJson(response, JavaServerStatusToken.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private JavaServerChallengeStatusToken retrieveChallengeStatusToken(@NonNull String hostname, int port) throws IOException {
|
||||||
|
log.info("Opening UDP connection to {}:{}...", hostname, port);
|
||||||
|
long before = System.currentTimeMillis(); // Timestamp before pinging
|
||||||
|
|
||||||
|
// Open a socket connection to the server
|
||||||
|
try (DatagramSocket socket = new DatagramSocket()) {
|
||||||
|
socket.setSoTimeout(500);
|
||||||
|
socket.connect(new InetSocketAddress(hostname, port));
|
||||||
|
|
||||||
|
long ping = System.currentTimeMillis() - before; // Calculate the ping
|
||||||
|
log.info("UDP Connection to {}:{} opened. Ping: {}ms", hostname, port, ping);
|
||||||
|
|
||||||
|
// Begin handshaking with the server
|
||||||
|
new JavaQueryHandshakeRequestPacket().process(socket);
|
||||||
|
JavaQueryHandshakeResponsePacket handshakeResponse = new JavaQueryHandshakeResponsePacket();
|
||||||
|
handshakeResponse.process(socket);
|
||||||
|
|
||||||
|
// Send the full stats request to the server, and await back the response
|
||||||
|
new JavaQueryFullStatRequestPacket(handshakeResponse.getResponse()).process(socket);
|
||||||
|
JavaQueryFullStatResponsePacket fullStatResponse = new JavaQueryFullStatResponsePacket();
|
||||||
|
fullStatResponse.process(socket);
|
||||||
|
|
||||||
|
// Return the challenge token
|
||||||
|
return JavaServerChallengeStatusToken.create(fullStatResponse.getResponse());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user