diff --git a/src/main/java/me/braydon/mc/common/DNSUtils.java b/src/main/java/me/braydon/mc/common/DNSUtils.java index 1b60125..98b87f7 100644 --- a/src/main/java/me/braydon/mc/common/DNSUtils.java +++ b/src/main/java/me/braydon/mc/common/DNSUtils.java @@ -26,11 +26,11 @@ package me.braydon.mc.common; import lombok.NonNull; import lombok.SneakyThrows; import lombok.experimental.UtilityClass; +import me.braydon.mc.model.dns.impl.ARecord; +import me.braydon.mc.model.dns.impl.SRVRecord; +import org.xbill.DNS.Lookup; import org.xbill.DNS.Record; -import org.xbill.DNS.*; - -import java.net.InetAddress; -import java.net.InetSocketAddress; +import org.xbill.DNS.Type; /** * @author Braydon @@ -47,19 +47,16 @@ public final class DNSUtils { * @return the resolved address and port, null if none */ @SneakyThrows - public static InetSocketAddress resolveSRV(@NonNull String hostname) { + public static SRVRecord resolveSRV(@NonNull String hostname) { Record[] records = new Lookup(SRV_QUERY_PREFIX.formatted(hostname), Type.SRV).run(); // Resolve SRV records if (records == null) { // No records exist return null; } - String host = null; - int port = -1; + SRVRecord result = null; for (Record record : records) { - SRVRecord srv = (SRVRecord) record; - host = srv.getTarget().toString().replaceFirst("\\.$", ""); - port = srv.getPort(); + result = new SRVRecord((org.xbill.DNS.SRVRecord) record); } - return host == null ? null : new InetSocketAddress(host, port); + return result; } /** @@ -70,15 +67,15 @@ public final class DNSUtils { * @return the resolved address, null if none */ @SneakyThrows - public static InetAddress resolveA(@NonNull String hostname) { + public static ARecord resolveA(@NonNull String hostname) { Record[] records = new Lookup(hostname, Type.A).run(); // Resolve A records if (records == null) { // No records exist return null; } - InetAddress address = null; + ARecord result = null; for (Record record : records) { - address = ((ARecord) record).getAddress(); + result = new ARecord((org.xbill.DNS.ARecord) record); } - return address; + return result; } } \ No newline at end of file diff --git a/src/main/java/me/braydon/mc/model/MinecraftServer.java b/src/main/java/me/braydon/mc/model/MinecraftServer.java index fdda300..0c72aeb 100644 --- a/src/main/java/me/braydon/mc/model/MinecraftServer.java +++ b/src/main/java/me/braydon/mc/model/MinecraftServer.java @@ -25,6 +25,7 @@ package me.braydon.mc.model; import lombok.*; import me.braydon.mc.common.ColorUtils; +import me.braydon.mc.model.dns.DNSRecord; import me.braydon.mc.model.token.JavaServerStatusToken; import me.braydon.mc.service.pinger.MinecraftServerPinger; import me.braydon.mc.service.pinger.impl.BedrockMinecraftServerPinger; @@ -57,6 +58,11 @@ public class MinecraftServer { */ @EqualsAndHashCode.Include private final int port; + /** + * The DNS records resolved for this server. + */ + @NonNull private final DNSRecord[] records; + /** * The player counts of this server. */ diff --git a/src/main/java/me/braydon/mc/model/dns/DNSRecord.java b/src/main/java/me/braydon/mc/model/dns/DNSRecord.java new file mode 100644 index 0000000..7949d43 --- /dev/null +++ b/src/main/java/me/braydon/mc/model/dns/DNSRecord.java @@ -0,0 +1,54 @@ +/* + * 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.dns; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +/** + * A representation of a DNS record. + * + * @author Braydon + */ +@AllArgsConstructor @Getter @ToString +public abstract class DNSRecord { + /** + * The type of this record. + */ + @NonNull private final Type type; + + /** + * The TTL (Time To Live) of this record. + */ + private final long ttl; + + /** + * Types of a record. + */ + public enum Type { + A, SRV + } +} \ No newline at end of file diff --git a/src/main/java/me/braydon/mc/model/dns/impl/ARecord.java b/src/main/java/me/braydon/mc/model/dns/impl/ARecord.java new file mode 100644 index 0000000..cc7f45c --- /dev/null +++ b/src/main/java/me/braydon/mc/model/dns/impl/ARecord.java @@ -0,0 +1,47 @@ +/* + * 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.dns.impl; + +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; +import me.braydon.mc.model.dns.DNSRecord; + +/** + * An A record implementation. + * + * @author Braydon + */ +@Getter @ToString(callSuper = true) +public final class ARecord extends DNSRecord { + /** + * The address of this record, null if unresolved. + */ + private final String address; + + public ARecord(@NonNull org.xbill.DNS.ARecord bootstrap) { + super(Type.A, bootstrap.getTTL()); + address = bootstrap.getAddress().getHostAddress(); + } +} \ No newline at end of file diff --git a/src/main/java/me/braydon/mc/model/dns/impl/SRVRecord.java b/src/main/java/me/braydon/mc/model/dns/impl/SRVRecord.java new file mode 100644 index 0000000..e0ff499 --- /dev/null +++ b/src/main/java/me/braydon/mc/model/dns/impl/SRVRecord.java @@ -0,0 +1,79 @@ +/* + * MIT License + * + * Copyright (c) 2024 Braydon (Rainnny). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package me.braydon.mc.model.dns.impl; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; +import me.braydon.mc.model.dns.DNSRecord; + +import java.net.InetSocketAddress; + +/** + * An SRV record implementation. + * + * @author Braydon + */ +@Getter @ToString(callSuper = true) +public final class SRVRecord extends DNSRecord { + /** + * The priority of this record. + */ + private final int priority; + + /** + * The weight of this record. + */ + private final int weight; + + /** + * The port of this record. + */ + private final int port; + + /** + * The target of this record. + */ + @NonNull private final String target; + + public SRVRecord(@NonNull org.xbill.DNS.SRVRecord bootstrap) { + super(Type.SRV, bootstrap.getTTL()); + priority = bootstrap.getPriority(); + weight = bootstrap.getWeight(); + port = bootstrap.getPort(); + target = bootstrap.getTarget().toString().replaceFirst("\\.$", ""); + } + + /** + * Get a socket address from + * the target and port. + * + * @return the socket address + */ + @NonNull @JsonIgnore + public InetSocketAddress getSocketAddress() { + return new InetSocketAddress(target, port); + } +} \ No newline at end of file diff --git a/src/main/java/me/braydon/mc/model/server/BedrockMinecraftServer.java b/src/main/java/me/braydon/mc/model/server/BedrockMinecraftServer.java index 7052e71..f61c5b1 100644 --- a/src/main/java/me/braydon/mc/model/server/BedrockMinecraftServer.java +++ b/src/main/java/me/braydon/mc/model/server/BedrockMinecraftServer.java @@ -25,6 +25,7 @@ package me.braydon.mc.model.server; import lombok.*; import me.braydon.mc.model.MinecraftServer; +import me.braydon.mc.model.dns.DNSRecord; /** * A Bedrock edition {@link MinecraftServer}. @@ -53,10 +54,10 @@ public final class BedrockMinecraftServer extends MinecraftServer { */ @NonNull private final GameMode gamemode; - private BedrockMinecraftServer(@NonNull String id, @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, players, motd); + private BedrockMinecraftServer(@NonNull String id, @NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records, + @NonNull Edition edition, @NonNull Version version, @NonNull Players players, @NonNull MOTD motd, + @NonNull GameMode gamemode) { + super(hostname, ip, port, records, players, motd); this.id = id; this.edition = edition; this.version = version; @@ -69,18 +70,20 @@ public final class BedrockMinecraftServer extends MinecraftServer { * @param hostname the hostname of the server * @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 * @return the Bedrock Minecraft server */ @NonNull - public static BedrockMinecraftServer create(@NonNull String hostname, String ip, int port, @NonNull String token) { + public static BedrockMinecraftServer create(@NonNull String hostname, String ip, int port, + @NonNull DNSRecord[] records, @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); + return new BedrockMinecraftServer(split[6], hostname, ip, port, records, edition, version, players, motd, gameMode); } /** diff --git a/src/main/java/me/braydon/mc/model/server/JavaMinecraftServer.java b/src/main/java/me/braydon/mc/model/server/JavaMinecraftServer.java index 1d5b257..f1d4c10 100644 --- a/src/main/java/me/braydon/mc/model/server/JavaMinecraftServer.java +++ b/src/main/java/me/braydon/mc/model/server/JavaMinecraftServer.java @@ -28,6 +28,7 @@ import lombok.*; 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.JavaServerStatusToken; import me.braydon.mc.service.MojangService; import net.md_5.bungee.api.chat.TextComponent; @@ -96,10 +97,10 @@ public final class JavaMinecraftServer extends MinecraftServer { */ private boolean mojangBanned; - private JavaMinecraftServer(@NonNull String hostname, String ip, int port, @NonNull Version version, - @NonNull Players players, @NonNull MOTD motd, Favicon favicon, ModInfo modInfo, - ForgeData forgeData, boolean previewsChat, boolean enforcesSecureChat, boolean preventsChatReports, boolean mojangBanned) { - super(hostname, ip, port, players, motd); + 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) { + super(hostname, ip, port, records, players, motd); this.version = version; this.favicon = favicon; this.modInfo = modInfo; @@ -116,16 +117,18 @@ public final class JavaMinecraftServer extends MinecraftServer { * @param hostname the hostname of the server * @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 * @return the Java Minecraft server */ @NonNull - public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, @NonNull JavaServerStatusToken token) { + 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; if (motdString == null) { // Not a string motd, convert from Json motdString = new TextComponent(ComponentSerializer.parse(AppConfig.GSON.toJson(token.getDescription()))).toLegacyText(); } - return new JavaMinecraftServer(hostname, ip, port, token.getVersion().detailedCopy(), Players.create(token.getPlayers()), + 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 ); diff --git a/src/main/java/me/braydon/mc/service/MojangService.java b/src/main/java/me/braydon/mc/service/MojangService.java index b81b728..fa5e452 100644 --- a/src/main/java/me/braydon/mc/service/MojangService.java +++ b/src/main/java/me/braydon/mc/service/MojangService.java @@ -42,6 +42,9 @@ import me.braydon.mc.model.cache.CachedMinecraftServer; import me.braydon.mc.model.cache.CachedPlayer; import me.braydon.mc.model.cache.CachedPlayerName; import me.braydon.mc.model.cache.CachedSkinPartTexture; +import me.braydon.mc.model.dns.DNSRecord; +import me.braydon.mc.model.dns.impl.ARecord; +import me.braydon.mc.model.dns.impl.SRVRecord; import me.braydon.mc.model.server.JavaMinecraftServer; import me.braydon.mc.model.token.MojangProfileToken; import me.braydon.mc.model.token.MojangUsernameToUUIDToken; @@ -55,7 +58,6 @@ import org.springframework.http.HttpMethod; import org.springframework.stereotype.Service; import java.io.InputStream; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -394,20 +396,25 @@ public final class MojangService { log.info("Found server in cache: {}", hostname); return cached.get(); } - InetSocketAddress address = platform == MinecraftServer.Platform.JAVA ? DNSUtils.resolveSRV(hostname) : null; // Resolve the SRV record - if (address != null) { // SRV was resolved, use the hostname and port - hostname = address.getHostName(); - port = address.getPort(); + List records = new ArrayList<>(); // The resolved DNS records for the server + + SRVRecord srvRecord = platform == MinecraftServer.Platform.JAVA ? DNSUtils.resolveSRV(hostname) : null; // Resolve the SRV record + if (srvRecord != null) { // SRV was resolved, use the hostname and port + records.add(srvRecord); // Going to need this for later + InetSocketAddress socketAddress = srvRecord.getSocketAddress(); + hostname = socketAddress.getHostName(); + port = socketAddress.getPort(); } - InetAddress inetAddress = DNSUtils.resolveA(hostname); // Resolve the hostname to an IP address - String ip = inetAddress == null ? null : inetAddress.getHostAddress(); // Get the IP address + ARecord aRecord = DNSUtils.resolveA(hostname); // Resolve the A record so we can get the IPv4 address + String ip = aRecord == null ? null : aRecord.getAddress(); // Get the IP address if (ip != null) { // Was the IP resolved? + records.add(aRecord); // Going to need this for later log.info("Resolved hostname: {} -> {}", hostname, ip); } // Build our server model, cache it, and then return it - MinecraftServer response = platform.getPinger().ping(hostname, ip, port); // Ping the server and await a response + MinecraftServer response = platform.getPinger().ping(hostname, ip, port, records.toArray(new DNSRecord[0])); // Ping the server and await a response if (response == null) { // No response from ping throw new ResourceNotFoundException("Server didn't respond to ping"); } diff --git a/src/main/java/me/braydon/mc/service/pinger/MinecraftServerPinger.java b/src/main/java/me/braydon/mc/service/pinger/MinecraftServerPinger.java index 7ff922a..d75bb91 100644 --- a/src/main/java/me/braydon/mc/service/pinger/MinecraftServerPinger.java +++ b/src/main/java/me/braydon/mc/service/pinger/MinecraftServerPinger.java @@ -25,6 +25,7 @@ package me.braydon.mc.service.pinger; import lombok.NonNull; import me.braydon.mc.model.MinecraftServer; +import me.braydon.mc.model.dns.DNSRecord; /** * A {@link MinecraftServerPinger} is @@ -40,7 +41,8 @@ public interface MinecraftServerPinger { * @param hostname the hostname of the server * @param ip the ip of the server, null if unresolved * @param port the port of the server + * @param records the DNS records of the server * @return the server that was pinged */ - T ping(@NonNull String hostname, String ip, int port); + T ping(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records); } \ No newline at end of file diff --git a/src/main/java/me/braydon/mc/service/pinger/impl/BedrockMinecraftServerPinger.java b/src/main/java/me/braydon/mc/service/pinger/impl/BedrockMinecraftServerPinger.java index f2e0e2d..cdd8ec2 100644 --- a/src/main/java/me/braydon/mc/service/pinger/impl/BedrockMinecraftServerPinger.java +++ b/src/main/java/me/braydon/mc/service/pinger/impl/BedrockMinecraftServerPinger.java @@ -29,6 +29,7 @@ import me.braydon.mc.common.packet.impl.bedrock.BedrockPacketUnconnectedPing; import me.braydon.mc.common.packet.impl.bedrock.BedrockPacketUnconnectedPong; 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.BedrockMinecraftServer; import me.braydon.mc.service.pinger.MinecraftServerPinger; @@ -54,10 +55,11 @@ public final class BedrockMinecraftServerPinger implements MinecraftServerPinger * @param hostname the hostname of the server * @param ip the ip of the server, null if unresolved * @param port the port of the server + * @param records the DNS records of the server * @return the server that was pinged */ @Override - public BedrockMinecraftServer ping(@NonNull String hostname, String ip, int port) { + public BedrockMinecraftServer ping(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records) { log.info("Pinging {}:{}...", hostname, port); long before = System.currentTimeMillis(); // Timestamp before pinging @@ -79,7 +81,7 @@ public final class BedrockMinecraftServerPinger implements MinecraftServerPinger 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 + return BedrockMinecraftServer.create(hostname, ip, port, records, response); // Return the server } catch (IOException ex) { if (ex instanceof UnknownHostException) { throw new BadRequestException("Unknown hostname: %s".formatted(hostname)); diff --git a/src/main/java/me/braydon/mc/service/pinger/impl/JavaMinecraftServerPinger.java b/src/main/java/me/braydon/mc/service/pinger/impl/JavaMinecraftServerPinger.java index 71daa7d..0e84ae0 100644 --- a/src/main/java/me/braydon/mc/service/pinger/impl/JavaMinecraftServerPinger.java +++ b/src/main/java/me/braydon/mc/service/pinger/impl/JavaMinecraftServerPinger.java @@ -31,6 +31,7 @@ import me.braydon.mc.common.packet.impl.java.JavaPacketStatusInStart; 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.JavaServerStatusToken; import me.braydon.mc.service.pinger.MinecraftServerPinger; @@ -56,10 +57,11 @@ public final class JavaMinecraftServerPinger implements MinecraftServerPinger