From 3faf2d33193d9baf5a05009ea8e3e8f850fe983e Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 21 Apr 2024 23:27:44 +0100 Subject: [PATCH] add location to the server response --- .gitignore | 3 + pom.xml | 14 +++ .../model/server/BedrockMinecraftServer.java | 9 +- .../model/server/JavaMinecraftServer.java | 11 +- .../backend/model/server/MinecraftServer.java | 53 ++++++++ .../backend/service/MaxMindService.java | 116 ++++++++++++++++++ .../impl/BedrockMinecraftServerPinger.java | 5 +- .../impl/JavaMinecraftServerPinger.java | 12 +- src/main/resources/application.yml | 5 + 9 files changed, 217 insertions(+), 11 deletions(-) create mode 100644 src/main/java/xyz/mcutils/backend/service/MaxMindService.java diff --git a/.gitignore b/.gitignore index 77440ae..2d06a61 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ git.properties pom.xml.versionsBackup application.yml target/ + +### MaxMind GeoIP2 +data/ diff --git a/pom.xml b/pom.xml index 947f2ee..be11eb6 100644 --- a/pom.xml +++ b/pom.xml @@ -165,6 +165,20 @@ compile + + + com.maxmind.geoip2 + geoip2 + 4.2.0 + + + + + org.codehaus.plexus + plexus-archiver + 4.9.1 + + org.springframework.boot diff --git a/src/main/java/xyz/mcutils/backend/model/server/BedrockMinecraftServer.java b/src/main/java/xyz/mcutils/backend/model/server/BedrockMinecraftServer.java index d8dd366..d6e39a3 100644 --- a/src/main/java/xyz/mcutils/backend/model/server/BedrockMinecraftServer.java +++ b/src/main/java/xyz/mcutils/backend/model/server/BedrockMinecraftServer.java @@ -32,8 +32,8 @@ public final class BedrockMinecraftServer extends MinecraftServer { 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, motd, players); + @NonNull GameMode gamemode, GeoLocation location) { + super(hostname, ip, port, records, motd, players, location); this.id = id; this.edition = edition; this.version = version; @@ -53,7 +53,7 @@ public final class BedrockMinecraftServer extends MinecraftServer { * @return the Bedrock Minecraft server */ @NonNull - public static BedrockMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, @NonNull String token) { + public static BedrockMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, GeoLocation location, @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]); @@ -70,7 +70,8 @@ public final class BedrockMinecraftServer extends MinecraftServer { version, players, motd, - gameMode + gameMode, + location ); } diff --git a/src/main/java/xyz/mcutils/backend/model/server/JavaMinecraftServer.java b/src/main/java/xyz/mcutils/backend/model/server/JavaMinecraftServer.java index 3e8a34b..b657981 100644 --- a/src/main/java/xyz/mcutils/backend/model/server/JavaMinecraftServer.java +++ b/src/main/java/xyz/mcutils/backend/model/server/JavaMinecraftServer.java @@ -65,10 +65,10 @@ public final class JavaMinecraftServer extends MinecraftServer { */ private boolean mojangBlocked; - public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, Players players, DNSRecord[] records, - @NonNull Version version, Favicon favicon, ForgeModInfo modInfo, ForgeData forgeData, - boolean preventsChatReports, boolean enforcesSecureChat, boolean previewsChat) { - super(hostname, ip, port, records, motd, players); + public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, Players players, GeoLocation location, + DNSRecord[] records, @NonNull Version version, Favicon favicon, ForgeModInfo modInfo, + ForgeData forgeData, boolean preventsChatReports, boolean enforcesSecureChat, boolean previewsChat) { + super(hostname, ip, port, records, motd, players, location); this.version = version; this.favicon = favicon; this.modInfo = modInfo; @@ -88,7 +88,7 @@ public final class JavaMinecraftServer extends MinecraftServer { * @return the Java Minecraft server */ @NonNull - public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, @NonNull JavaServerStatusToken token) { + public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, GeoLocation location, @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(Main.GSON.toJson(token.getDescription()))).toLegacyText(); @@ -99,6 +99,7 @@ public final class JavaMinecraftServer extends MinecraftServer { port, MinecraftServer.MOTD.create(motdString), token.getPlayers(), + location, records, token.getVersion().detailedCopy(), JavaMinecraftServer.Favicon.create(token.getFavicon(), ServerUtils.getAddress(hostname, port)), diff --git a/src/main/java/xyz/mcutils/backend/model/server/MinecraftServer.java b/src/main/java/xyz/mcutils/backend/model/server/MinecraftServer.java index 2e24aaa..fd23887 100644 --- a/src/main/java/xyz/mcutils/backend/model/server/MinecraftServer.java +++ b/src/main/java/xyz/mcutils/backend/model/server/MinecraftServer.java @@ -1,5 +1,6 @@ package xyz.mcutils.backend.model.server; +import com.maxmind.geoip2.model.CityResponse; import io.micrometer.common.lang.NonNull; import lombok.*; import xyz.mcutils.backend.common.ColorUtils; @@ -48,6 +49,11 @@ public class MinecraftServer { */ private final Players players; + /** + * The location of the server. + */ + private final GeoLocation location; + /** * A platform a Minecraft * server can operate on. @@ -147,4 +153,51 @@ public class MinecraftServer { @NonNull private final String name; } } + + /** + * The location of the server. + */ + @AllArgsConstructor @Getter + public static class GeoLocation { + /** + * The country of the server. + */ + private final String country; + + /** + * The region of the server. + */ + private final String region; + + /** + * The city of the server. + */ + private final String city; + + /** + * The latitude of the server. + */ + private final double latitude; + + /** + * The longitude of the server. + */ + private final double longitude; + + /** + * Gets the location of the server from Maxmind. + * + * @param response the response from Maxmind + * @return the location of the server + */ + public static GeoLocation fromMaxMind(CityResponse response) { + return new GeoLocation( + response.getCountry().getName(), + response.getMostSpecificSubdivision().getName(), + response.getCity().getName(), + response.getLocation().getLatitude(), + response.getLocation().getLongitude() + ); + } + } } \ No newline at end of file diff --git a/src/main/java/xyz/mcutils/backend/service/MaxMindService.java b/src/main/java/xyz/mcutils/backend/service/MaxMindService.java new file mode 100644 index 0000000..bdd13ca --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/service/MaxMindService.java @@ -0,0 +1,116 @@ +package xyz.mcutils.backend.service; + +import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import com.maxmind.geoip2.model.CityResponse; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; +import org.codehaus.plexus.archiver.tar.TarGZipUnArchiver; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import xyz.mcutils.backend.Main; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; + +@Service +@Log4j2(topic = "MaxMind Service") +public class MaxMindService { + /** + * The MaxMind database. + */ + private static DatabaseReader database; + + /** + * The location of the MaxMind database. + */ + private final String databaseName = "maxmind.mmdb"; + + /** + * The MaxMind license key. + */ + private final String maxMindLicense; + + public MaxMindService(@Value("${maxmind.license}") String maxMindLicense) { + this.maxMindLicense = maxMindLicense; + + File databaseFile = loadDatabase(); + try { + database = new DatabaseReader.Builder(databaseFile).build(); + log.info("Loaded the MaxMind database from '{}'", databaseFile.getAbsolutePath()); + } catch (Exception ex) { + log.error("Failed to load the MaxMind database, please check the configuration and try again", ex); + System.exit(1); + } + } + + /** + * Lookup the GeoIP information for the query. + * + * @param query The query to lookup + * @return The GeoIP information + */ + public static CityResponse lookup(String query) { + try { + return database.city(InetAddress.getByName(query)); + } catch (IOException | GeoIp2Exception e) { + log.error("Failed to lookup the GeoIP information for '{}'", query, e); + throw new RuntimeException("Failed to lookup the GeoIP information for '%s'".formatted(query)); + } + } + + @SneakyThrows + private File loadDatabase() { + File database = new File("data", databaseName); + if (database.exists()) { + return database; + } + + // Ensure the parent directories exist + database.getParentFile().mkdirs(); + + String downloadUrl = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz"; + HttpResponse response = Main.HTTP_CLIENT.send(HttpRequest.newBuilder() + .uri(URI.create(downloadUrl.formatted(maxMindLicense))) + .build(), HttpResponse.BodyHandlers.ofFile(Files.createTempFile("maxmind", ".tar.gz"))); + Path downloadedFile = response.body(); + + File tempDir = Files.createTempDirectory("maxmind").toFile(); + + TarGZipUnArchiver archiver = new TarGZipUnArchiver(); + archiver.setSourceFile(downloadedFile.toFile()); + archiver.setDestDirectory(tempDir); + archiver.extract(); + + File[] files = tempDir.listFiles(); + if (files == null || files.length == 0) { + log.error("Failed to extract the MaxMind database"); + System.exit(1); + } + + // Search for the database file + for (File file : files) { + // The database is in a subdirectory + if (!file.isDirectory()) { + continue; + } + + // Get the database file + File databaseFile = new File(file, "GeoLite2-City.mmdb"); + if (!databaseFile.exists()) { + log.error("Failed to find the MaxMind database in the extracted files"); + continue; + } + Files.copy(databaseFile.toPath(), database.toPath()); + } + + log.info("Downloaded and extracted the MaxMind database to '{}'", database.getAbsolutePath()); + return database; + } +} diff --git a/src/main/java/xyz/mcutils/backend/service/pinger/impl/BedrockMinecraftServerPinger.java b/src/main/java/xyz/mcutils/backend/service/pinger/impl/BedrockMinecraftServerPinger.java index 545fc72..8b4eb9c 100644 --- a/src/main/java/xyz/mcutils/backend/service/pinger/impl/BedrockMinecraftServerPinger.java +++ b/src/main/java/xyz/mcutils/backend/service/pinger/impl/BedrockMinecraftServerPinger.java @@ -7,6 +7,8 @@ import xyz.mcutils.backend.exception.impl.BadRequestException; import xyz.mcutils.backend.exception.impl.ResourceNotFoundException; import xyz.mcutils.backend.model.dns.DNSRecord; import xyz.mcutils.backend.model.server.BedrockMinecraftServer; +import xyz.mcutils.backend.model.server.MinecraftServer; +import xyz.mcutils.backend.service.MaxMindService; import xyz.mcutils.backend.service.pinger.MinecraftServerPinger; import java.io.IOException; @@ -55,7 +57,8 @@ public final class BedrockMinecraftServerPinger implements MinecraftServerPinger if (response == null) { // No pong response throw new ResourceNotFoundException("Server '%s' didn't respond to ping".formatted(hostname)); } - return BedrockMinecraftServer.create(hostname, ip, port, records, response); // Return the server + return BedrockMinecraftServer.create(hostname, ip, port, records, + MinecraftServer.GeoLocation.fromMaxMind(MaxMindService.lookup(hostname)), 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/xyz/mcutils/backend/service/pinger/impl/JavaMinecraftServerPinger.java b/src/main/java/xyz/mcutils/backend/service/pinger/impl/JavaMinecraftServerPinger.java index 5ef49c3..71226e1 100644 --- a/src/main/java/xyz/mcutils/backend/service/pinger/impl/JavaMinecraftServerPinger.java +++ b/src/main/java/xyz/mcutils/backend/service/pinger/impl/JavaMinecraftServerPinger.java @@ -9,7 +9,9 @@ import xyz.mcutils.backend.exception.impl.BadRequestException; import xyz.mcutils.backend.exception.impl.ResourceNotFoundException; import xyz.mcutils.backend.model.dns.DNSRecord; import xyz.mcutils.backend.model.server.JavaMinecraftServer; +import xyz.mcutils.backend.model.server.MinecraftServer; import xyz.mcutils.backend.model.token.JavaServerStatusToken; +import xyz.mcutils.backend.service.MaxMindService; import xyz.mcutils.backend.service.pinger.MinecraftServerPinger; import java.io.DataInputStream; @@ -24,6 +26,13 @@ import java.net.*; public final class JavaMinecraftServerPinger implements MinecraftServerPinger { private static final int TIMEOUT = 1500; // 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 JavaMinecraftServer ping(String hostname, String ip, int port, DNSRecord[] records) { log.info("Pinging {}:{}...", hostname, port); @@ -43,7 +52,8 @@ public final class JavaMinecraftServerPinger implements MinecraftServerPinger