add bedrock server support
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 20s
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 20s
This commit is contained in:
parent
5ad2f438d1
commit
2ba9651161
@ -13,10 +13,7 @@ public class ServerUtils {
|
||||
*/
|
||||
public static Tuple<String, Integer> getHostnameAndPort(String hostname) {
|
||||
String[] split = hostname.split(":");
|
||||
if (split.length == 1) {
|
||||
return new Tuple<>(split[0], 25565);
|
||||
}
|
||||
return new Tuple<>(split[0], Integer.parseInt(split[1]));
|
||||
return new Tuple<>(split[0], split.length == 1 ? -1 : Integer.parseInt(split[1]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,23 @@
|
||||
package cc.fascinated.common.packet;
|
||||
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramSocket;
|
||||
|
||||
/**
|
||||
* Represents a packet in the
|
||||
* Minecraft Bedrock protocol.
|
||||
*
|
||||
* @author Braydon
|
||||
* @see <a href="https://wiki.vg/Raknet_Protocol">Protocol Docs</a>
|
||||
*/
|
||||
public interface MinecraftBedrockPacket {
|
||||
/**
|
||||
* 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;
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package cc.fascinated.common.packet.impl.bedrock;
|
||||
|
||||
import cc.fascinated.common.packet.MinecraftBedrockPacket;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* This packet is sent by the client to the server to
|
||||
* request a pong response from the server. The server
|
||||
* will respond with a string containing the server's status.
|
||||
*
|
||||
* @author Braydon
|
||||
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Ping">Protocol Docs</a>
|
||||
*/
|
||||
public final class BedrockPacketUnconnectedPing implements MinecraftBedrockPacket {
|
||||
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 };
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// Construct the packet buffer
|
||||
ByteBuffer buffer = ByteBuffer.allocate(33).order(ByteOrder.LITTLE_ENDIAN);;
|
||||
buffer.put(ID); // Packet ID
|
||||
buffer.putLong(System.currentTimeMillis()); // Timestamp
|
||||
buffer.put(MAGIC); // Magic
|
||||
buffer.putLong(0L); // Client GUID
|
||||
|
||||
// Send the packet
|
||||
socket.send(new DatagramPacket(buffer.array(), 0, buffer.limit()));
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package cc.fascinated.common.packet.impl.bedrock;
|
||||
|
||||
import cc.fascinated.common.packet.MinecraftBedrockPacket;
|
||||
import cc.fascinated.model.server.BedrockMinecraftServer;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* This packet is sent by the server to the client in
|
||||
* response to the {@link BedrockPacketUnconnectedPing}.
|
||||
*
|
||||
* @author Braydon
|
||||
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Pong">Protocol Docs</a>
|
||||
*/
|
||||
@Getter
|
||||
public final class BedrockPacketUnconnectedPong implements MinecraftBedrockPacket {
|
||||
private static final byte ID = 0x1C; // The ID of the packet
|
||||
|
||||
/**
|
||||
* The response from the server, null if none.
|
||||
*/
|
||||
private 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[2048];
|
||||
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
|
||||
socket.receive(receivePacket);
|
||||
|
||||
// Construct a buffer from the received packet
|
||||
ByteBuffer buffer = ByteBuffer.wrap(receivePacket.getData()).order(ByteOrder.LITTLE_ENDIAN);
|
||||
byte id = buffer.get(); // The received packet id
|
||||
if (id == ID) {
|
||||
String response = new String(buffer.array(), StandardCharsets.UTF_8).trim(); // Extract the response
|
||||
|
||||
// Trim the length of the response (short) from the
|
||||
// start of the string, which begins with the edition name
|
||||
for (BedrockMinecraftServer.Edition edition : BedrockMinecraftServer.Edition.values()) {
|
||||
int startIndex = response.indexOf(edition.name());
|
||||
if (startIndex != -1) {
|
||||
response = response.substring(startIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.response = response;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package cc.fascinated.model.mojang;
|
||||
|
||||
import cc.fascinated.model.server.JavaMinecraftServer;
|
||||
import cc.fascinated.model.server.MinecraftServer;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
@ -19,7 +20,7 @@ public final class JavaServerStatusToken {
|
||||
/**
|
||||
* The players on the server.
|
||||
*/
|
||||
private final JavaMinecraftServer.Players players;
|
||||
private final MinecraftServer.Players players;
|
||||
|
||||
/**
|
||||
* The motd of the server.
|
||||
|
@ -0,0 +1,109 @@
|
||||
package cc.fascinated.model.server;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* A Bedrock edition {@link MinecraftServer}.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Getter @ToString(callSuper = true) @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
|
||||
public final class BedrockMinecraftServer extends MinecraftServer {
|
||||
/**
|
||||
* The unique ID of this server.
|
||||
*/
|
||||
@EqualsAndHashCode.Include @NonNull private final String uniqueId;
|
||||
|
||||
/**
|
||||
* The edition of this server.
|
||||
*/
|
||||
@NonNull private final Edition edition;
|
||||
|
||||
/**
|
||||
* The version information of this server.
|
||||
*/
|
||||
@NonNull private final Version version;
|
||||
|
||||
/**
|
||||
* The gamemode of this server.
|
||||
*/
|
||||
@NonNull private final GameMode gamemode;
|
||||
|
||||
private BedrockMinecraftServer(@NonNull String uniqueId, @NonNull String hostname, String ip, int port,
|
||||
@NonNull Edition edition, @NonNull Version version, @NonNull Players players,
|
||||
@NonNull MOTD motd, @NonNull GameMode gamemode) {
|
||||
super(hostname, ip, port, motd, players);
|
||||
this.uniqueId = uniqueId;
|
||||
this.edition = edition;
|
||||
this.version = version;
|
||||
this.gamemode = gamemode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Bedrock Minecraft server.
|
||||
*
|
||||
* @param hostname the hostname of the server
|
||||
* @param ip the IP address of the server
|
||||
* @param port the port of the server
|
||||
* @param token the status token
|
||||
* @return the Bedrock Minecraft server
|
||||
*/
|
||||
@NonNull
|
||||
public static BedrockMinecraftServer create(@NonNull String hostname, String ip, int port, @NonNull String token) {
|
||||
String[] split = token.split(";"); // Split the token
|
||||
Edition edition = Edition.valueOf(split[0]);
|
||||
Version version = new Version(Integer.parseInt(split[2]), split[3]);
|
||||
Players players = new Players(Integer.parseInt(split[4]), Integer.parseInt(split[5]), null);
|
||||
MOTD motd = MOTD.create(split[1] + "\n" + split[7]);
|
||||
GameMode gameMode = new GameMode(split[8], Integer.parseInt(split[9]));
|
||||
return new BedrockMinecraftServer(split[6], hostname, ip, port, edition, version, players, motd, gameMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* The edition of a Bedrock server.
|
||||
*/
|
||||
@AllArgsConstructor @Getter
|
||||
public enum Edition {
|
||||
/**
|
||||
* Minecraft: Pocket Edition.
|
||||
*/
|
||||
MCPE,
|
||||
|
||||
/**
|
||||
* Minecraft: Education Edition.
|
||||
*/
|
||||
MCEE
|
||||
}
|
||||
|
||||
/**
|
||||
* Version information for a server.
|
||||
*/
|
||||
@AllArgsConstructor @Getter @ToString
|
||||
public static class Version {
|
||||
/**
|
||||
* The protocol version of the server.
|
||||
*/
|
||||
private final int protocol;
|
||||
|
||||
/**
|
||||
* The version name of the server.
|
||||
*/
|
||||
@NonNull private final String name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The gamemode of a server.
|
||||
*/
|
||||
@AllArgsConstructor @Getter @ToString
|
||||
public static class GameMode {
|
||||
/**
|
||||
* The name of this gamemode.
|
||||
*/
|
||||
@NonNull private final String name;
|
||||
|
||||
/**
|
||||
* The numeric of this gamemode.
|
||||
*/
|
||||
private final int numericId;
|
||||
}
|
||||
}
|
@ -12,8 +12,6 @@ import lombok.Setter;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@ -25,11 +23,6 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
||||
*/
|
||||
@NonNull private final Version version;
|
||||
|
||||
/**
|
||||
* The players on the server.
|
||||
*/
|
||||
private Players players;
|
||||
|
||||
/**
|
||||
* The favicon of the server.
|
||||
*/
|
||||
@ -41,9 +34,8 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
||||
private boolean mojangBanned;
|
||||
|
||||
public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, @NonNull Version version, Players players, Favicon favicon) {
|
||||
super(hostname, ip, port, motd);
|
||||
super(hostname, ip, port, motd, players);
|
||||
this.version = version;
|
||||
this.players = players;
|
||||
this.favicon = favicon;
|
||||
}
|
||||
|
||||
@ -117,25 +109,6 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
||||
|
||||
}
|
||||
|
||||
@Getter @AllArgsConstructor
|
||||
public static class Players {
|
||||
|
||||
/**
|
||||
* The maximum amount of players the server can hold.
|
||||
*/
|
||||
private final int max;
|
||||
|
||||
/**
|
||||
* The amount of players currently online.
|
||||
*/
|
||||
private final int online;
|
||||
|
||||
/**
|
||||
* The sample of players currently online.
|
||||
*/
|
||||
private final String[] sample;
|
||||
}
|
||||
|
||||
@Getter @AllArgsConstructor
|
||||
public static class Favicon {
|
||||
|
||||
|
@ -2,6 +2,7 @@ package cc.fascinated.model.server;
|
||||
|
||||
import cc.fascinated.common.ColorUtils;
|
||||
import cc.fascinated.service.pinger.MinecraftServerPinger;
|
||||
import cc.fascinated.service.pinger.impl.BedrockMinecraftServerPinger;
|
||||
import cc.fascinated.service.pinger.impl.JavaMinecraftServerPinger;
|
||||
import io.micrometer.common.lang.NonNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
@ -9,17 +10,38 @@ import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor @Getter @ToString
|
||||
public class MinecraftServer {
|
||||
/**
|
||||
* The hostname of the server.
|
||||
*/
|
||||
private final String hostname;
|
||||
|
||||
/**
|
||||
* The IP address of the server.
|
||||
*/
|
||||
private final String ip;
|
||||
|
||||
/**
|
||||
* The port of the server.
|
||||
*/
|
||||
private final int port;
|
||||
|
||||
/**
|
||||
* The motd for the server.
|
||||
*/
|
||||
private final MOTD motd;
|
||||
|
||||
/**
|
||||
* The players on the server.
|
||||
*/
|
||||
private Players players;
|
||||
|
||||
/**
|
||||
* A platform a Minecraft
|
||||
* server can operate on.
|
||||
@ -29,7 +51,12 @@ public class MinecraftServer {
|
||||
/**
|
||||
* The Java edition of Minecraft.
|
||||
*/
|
||||
JAVA(new JavaMinecraftServerPinger(), 25565);
|
||||
JAVA(new JavaMinecraftServerPinger(), 25565),
|
||||
|
||||
/**
|
||||
* The Bedrock edition of Minecraft.
|
||||
*/
|
||||
BEDROCK(new BedrockMinecraftServerPinger(), 19132);
|
||||
|
||||
/**
|
||||
* The server pinger for this platform.
|
||||
@ -77,4 +104,41 @@ public class MinecraftServer {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Player count data for a server.
|
||||
*/
|
||||
@AllArgsConstructor @Getter @ToString
|
||||
public static class Players {
|
||||
/**
|
||||
* The online players on this server.
|
||||
*/
|
||||
private final int online;
|
||||
|
||||
/**
|
||||
* The maximum allowed players on this server.
|
||||
*/
|
||||
private final int max;
|
||||
|
||||
/**
|
||||
* A sample of players on this server, null or empty if no sample.
|
||||
*/
|
||||
private final Sample[] sample;
|
||||
|
||||
/**
|
||||
* A sample player.
|
||||
*/
|
||||
@AllArgsConstructor @Getter @ToString
|
||||
public static class Sample {
|
||||
/**
|
||||
* The unique id of this player.
|
||||
*/
|
||||
@NonNull private final UUID id;
|
||||
|
||||
/**
|
||||
* The name of this player.
|
||||
*/
|
||||
@NonNull private final String name;
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,6 @@ import io.micrometer.common.lang.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import net.jodah.expiringmap.ExpirationPolicy;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
@ -38,14 +38,15 @@ public class ServerService {
|
||||
* @return the server
|
||||
*/
|
||||
public CachedMinecraftServer getServer(String platformName, String hostname, int port) {
|
||||
log.info("Getting server: {} {}:{}", platformName, hostname, port);
|
||||
MinecraftServer.Platform platform = EnumUtils.getEnumConstant(MinecraftServer.Platform.class, platformName.toUpperCase());
|
||||
if (platform == null) {
|
||||
log.info("Invalid platform: {} for server {}:{}", platformName, hostname, port);
|
||||
log.info("Invalid platform: {} for server {}", platformName, hostname);
|
||||
throw new BadRequestException("Invalid platform: %s".formatted(platformName));
|
||||
}
|
||||
port = port == -1 ? platform.getDefaultPort() : port; // Use the default port if the port is -1 (not provided)
|
||||
String key = "%s-%s:%s".formatted(platformName, hostname, port);
|
||||
|
||||
log.info("Getting server: {}:{}", hostname, port);
|
||||
Optional<CachedMinecraftServer> cached = serverCacheRepository.findById(key);
|
||||
if (cached.isPresent()) {
|
||||
log.info("Server {}:{} is cached", hostname, port);
|
||||
@ -54,7 +55,6 @@ public class ServerService {
|
||||
|
||||
InetSocketAddress address = platform == MinecraftServer.Platform.JAVA ? DNSUtils.resolveSRV(hostname) : null;
|
||||
if (address != null) {
|
||||
port = port != -1 ? port : platform.getDefaultPort(); // If the port is -1, set it to the default port
|
||||
hostname = address.getHostName();
|
||||
}
|
||||
|
||||
@ -65,8 +65,7 @@ public class ServerService {
|
||||
);
|
||||
|
||||
if (platform == MinecraftServer.Platform.JAVA) { // Check if the server is blocked by Mojang
|
||||
JavaMinecraftServer javaServer = (JavaMinecraftServer) server.getServer();
|
||||
javaServer.setMojangBanned(mojangService.isServerBlocked(hostname));
|
||||
((JavaMinecraftServer) server.getServer()).setMojangBanned(mojangService.isServerBlocked(hostname));
|
||||
}
|
||||
|
||||
log.info("Found server: {}:{}", hostname, port);
|
||||
|
@ -0,0 +1,72 @@
|
||||
package cc.fascinated.service.pinger.impl;
|
||||
|
||||
import cc.fascinated.common.DNSUtils;
|
||||
import cc.fascinated.common.packet.impl.bedrock.BedrockPacketUnconnectedPing;
|
||||
import cc.fascinated.common.packet.impl.bedrock.BedrockPacketUnconnectedPong;
|
||||
import cc.fascinated.exception.impl.BadRequestException;
|
||||
import cc.fascinated.exception.impl.ResourceNotFoundException;
|
||||
import cc.fascinated.model.server.BedrockMinecraftServer;
|
||||
import cc.fascinated.service.pinger.MinecraftServerPinger;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
|
||||
/**
|
||||
* The {@link MinecraftServerPinger} for pinging
|
||||
* {@link BedrockMinecraftServer} over UDP.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Log4j2(topic = "Bedrock MC Server Pinger")
|
||||
public final class BedrockMinecraftServerPinger implements MinecraftServerPinger<BedrockMinecraftServer> {
|
||||
private static final int TIMEOUT = 3000; // The timeout for the socket
|
||||
|
||||
/**
|
||||
* Ping the server with the given hostname and port.
|
||||
*
|
||||
* @param hostname the hostname of the server
|
||||
* @param port the port of the server
|
||||
* @return the server that was pinged
|
||||
*/
|
||||
@Override
|
||||
public BedrockMinecraftServer ping(@NonNull String hostname, int port) {
|
||||
InetAddress inetAddress = DNSUtils.resolveA(hostname); // Resolve the hostname to an IP address
|
||||
String ip = inetAddress == null ? null : inetAddress.getHostAddress(); // Get the IP address
|
||||
if (ip != null) { // Was the IP resolved?
|
||||
log.info("Resolved hostname: {} -> {}", hostname, ip);
|
||||
}
|
||||
log.info("Pinging {}:{}...", hostname, port);
|
||||
long before = System.currentTimeMillis(); // Timestamp before pinging
|
||||
|
||||
// Open a socket connection to the server
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT);
|
||||
socket.connect(new InetSocketAddress(hostname, port));
|
||||
|
||||
long ping = System.currentTimeMillis() - before; // Calculate the ping
|
||||
log.info("Pinged {}:{} in {}ms", hostname, port, ping);
|
||||
|
||||
// Send the unconnected ping packet
|
||||
new BedrockPacketUnconnectedPing().process(socket);
|
||||
|
||||
// Handle the received unconnected pong packet
|
||||
BedrockPacketUnconnectedPong unconnectedPong = new BedrockPacketUnconnectedPong();
|
||||
unconnectedPong.process(socket);
|
||||
String response = unconnectedPong.getResponse();
|
||||
if (response == null) { // No pong response
|
||||
throw new ResourceNotFoundException("Server didn't respond to ping");
|
||||
}
|
||||
return BedrockMinecraftServer.create(hostname, ip, port, response); // Return the server
|
||||
} catch (IOException ex) {
|
||||
if (ex instanceof UnknownHostException) {
|
||||
throw new BadRequestException("Unknown hostname: %s".formatted(hostname));
|
||||
} else if (ex instanceof SocketTimeoutException) {
|
||||
throw new ResourceNotFoundException(ex);
|
||||
}
|
||||
log.error("An error occurred pinging %s:%s:".formatted(hostname, port), ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -9,7 +9,6 @@ import cc.fascinated.exception.impl.BadRequestException;
|
||||
import cc.fascinated.exception.impl.ResourceNotFoundException;
|
||||
import cc.fascinated.model.mojang.JavaServerStatusToken;
|
||||
import cc.fascinated.model.server.JavaMinecraftServer;
|
||||
import cc.fascinated.model.server.MinecraftServer;
|
||||
import cc.fascinated.service.pinger.MinecraftServerPinger;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user