Add Java server querying! (:
Some checks failed
Deploy API / docker (17, 3.8.5) (push) Has been cancelled

This commit is contained in:
Braydon 2024-04-22 20:49:35 -04:00
parent 345e1532a4
commit c689434ec4
16 changed files with 640 additions and 63 deletions

@ -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
* @see <a href="https://wiki.vg/Protocol">Protocol Docs</a>
*/
public abstract class MinecraftJavaPacket {
public abstract class TCPPacket {
/**
* Process this packet.
*

@ -35,12 +35,12 @@ import java.net.DatagramSocket;
* @author Braydon
* @see <a href="https://wiki.vg/Raknet_Protocol">Protocol Docs</a>
*/
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;
}

@ -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 <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[] 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.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 <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Pong">Protocol Docs</a>
*/
@Getter
public final class BedrockPacketUnconnectedPong implements MinecraftBedrockPacket {
public final class BedrockUnconnectedPongPacket extends UDPPacket {
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
* 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 <a href="https://wiki.vg/Protocol#Handshake">Protocol Docs</a>
*/
@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

@ -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 <a href="https://wiki.vg/Protocol#Status_Request">Protocol Docs</a>
*/
@Getter
public final class JavaPacketStatusInStart extends MinecraftJavaPacket {
public final class JavaStatusInStartPacket extends TCPPacket {
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.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.
* <p>
@ -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<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.
* <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
Optional<CachedMinecraftServer> 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<CachedMinecraftServer> 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<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
@ -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;

@ -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

@ -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<Ja
@Override
public JavaMinecraftServer ping(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records) {
log.info("Pinging {}:{}...", 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("Pinged {}:{} in {}ms", hostname, port, ping);
// Open data streams to begin packet transaction
try (DataInputStream inputStream = new DataInputStream(socket.getInputStream());
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream())) {
// Begin handshaking with the server
new JavaPacketHandshakingInSetProtocol(hostname, port, JavaMinecraftVersion.getMinimumVersion().getProtocol())
.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");
try {
// Ping the server and retrieve both the status token, and the challenge status token
JavaServerStatusToken statusToken = retrieveStatusToken(hostname, port);
JavaServerChallengeStatusToken challengeStatusToken = null;
try {
challengeStatusToken = retrieveChallengeStatusToken(hostname, port);
} catch (Exception ex) {
// An exception will be raised if querying
// is disabled on the server. If the exception
// is not caused by querying being disabled, we
// want to log the error.
if (!(ex instanceof IOException)) {
log.error("Failed retrieving challenge status token for %s:%s:".formatted(hostname, port), ex);
}
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) {
if (ex instanceof UnknownHostException) {
throw new BadRequestException("Unknown hostname: %s".formatted(hostname));
@ -100,4 +97,73 @@ public final class JavaMinecraftServerPinger implements MinecraftServerPinger<Ja
}
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());
}
}
}