add location to the server response

This commit is contained in:
Lee 2024-04-21 23:27:44 +01:00
parent bf992713dc
commit 3faf2d3319
9 changed files with 217 additions and 11 deletions

3
.gitignore vendored

@ -29,3 +29,6 @@ git.properties
pom.xml.versionsBackup pom.xml.versionsBackup
application.yml application.yml
target/ target/
### MaxMind GeoIP2
data/

14
pom.xml

@ -165,6 +165,20 @@
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<!-- GeoIP - IP Lookups -->
<dependency>
<groupId>com.maxmind.geoip2</groupId>
<artifactId>geoip2</artifactId>
<version>4.2.0</version>
</dependency>
<!-- Archive Utilities -->
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-archiver</artifactId>
<version>4.9.1</version>
</dependency>
<!-- Tests --> <!-- Tests -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

@ -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, 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 Edition edition, @NonNull Version version, @NonNull Players players, @NonNull MOTD motd,
@NonNull GameMode gamemode) { @NonNull GameMode gamemode, GeoLocation location) {
super(hostname, ip, port, records, motd, players); super(hostname, ip, port, records, motd, players, location);
this.id = id; this.id = id;
this.edition = edition; this.edition = edition;
this.version = version; this.version = version;
@ -53,7 +53,7 @@ public final class BedrockMinecraftServer extends MinecraftServer {
* @return the Bedrock Minecraft server * @return the Bedrock Minecraft server
*/ */
@NonNull @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 String[] split = token.split(";"); // Split the token
Edition edition = Edition.valueOf(split[0]); Edition edition = Edition.valueOf(split[0]);
Version version = new Version(Integer.parseInt(split[2]), split[3]); Version version = new Version(Integer.parseInt(split[2]), split[3]);
@ -70,7 +70,8 @@ public final class BedrockMinecraftServer extends MinecraftServer {
version, version,
players, players,
motd, motd,
gameMode gameMode,
location
); );
} }

@ -65,10 +65,10 @@ public final class JavaMinecraftServer extends MinecraftServer {
*/ */
private boolean mojangBlocked; private boolean mojangBlocked;
public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, Players players, DNSRecord[] records, public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, Players players, GeoLocation location,
@NonNull Version version, Favicon favicon, ForgeModInfo modInfo, ForgeData forgeData, DNSRecord[] records, @NonNull Version version, Favicon favicon, ForgeModInfo modInfo,
boolean preventsChatReports, boolean enforcesSecureChat, boolean previewsChat) { ForgeData forgeData, boolean preventsChatReports, boolean enforcesSecureChat, boolean previewsChat) {
super(hostname, ip, port, records, motd, players); super(hostname, ip, port, records, motd, players, location);
this.version = version; this.version = version;
this.favicon = favicon; this.favicon = favicon;
this.modInfo = modInfo; this.modInfo = modInfo;
@ -88,7 +88,7 @@ public final class JavaMinecraftServer extends MinecraftServer {
* @return the Java Minecraft server * @return the Java Minecraft server
*/ */
@NonNull @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; String motdString = token.getDescription() instanceof String ? (String) token.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(Main.GSON.toJson(token.getDescription()))).toLegacyText(); motdString = new TextComponent(ComponentSerializer.parse(Main.GSON.toJson(token.getDescription()))).toLegacyText();
@ -99,6 +99,7 @@ public final class JavaMinecraftServer extends MinecraftServer {
port, port,
MinecraftServer.MOTD.create(motdString), MinecraftServer.MOTD.create(motdString),
token.getPlayers(), token.getPlayers(),
location,
records, records,
token.getVersion().detailedCopy(), token.getVersion().detailedCopy(),
JavaMinecraftServer.Favicon.create(token.getFavicon(), ServerUtils.getAddress(hostname, port)), JavaMinecraftServer.Favicon.create(token.getFavicon(), ServerUtils.getAddress(hostname, port)),

@ -1,5 +1,6 @@
package xyz.mcutils.backend.model.server; package xyz.mcutils.backend.model.server;
import com.maxmind.geoip2.model.CityResponse;
import io.micrometer.common.lang.NonNull; import io.micrometer.common.lang.NonNull;
import lombok.*; import lombok.*;
import xyz.mcutils.backend.common.ColorUtils; import xyz.mcutils.backend.common.ColorUtils;
@ -48,6 +49,11 @@ public class MinecraftServer {
*/ */
private final Players players; private final Players players;
/**
* The location of the server.
*/
private final GeoLocation location;
/** /**
* A platform a Minecraft * A platform a Minecraft
* server can operate on. * server can operate on.
@ -147,4 +153,51 @@ public class MinecraftServer {
@NonNull private final String name; @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()
);
}
}
} }

@ -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<Path> 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;
}
}

@ -7,6 +7,8 @@ import xyz.mcutils.backend.exception.impl.BadRequestException;
import xyz.mcutils.backend.exception.impl.ResourceNotFoundException; import xyz.mcutils.backend.exception.impl.ResourceNotFoundException;
import xyz.mcutils.backend.model.dns.DNSRecord; import xyz.mcutils.backend.model.dns.DNSRecord;
import xyz.mcutils.backend.model.server.BedrockMinecraftServer; 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 xyz.mcutils.backend.service.pinger.MinecraftServerPinger;
import java.io.IOException; import java.io.IOException;
@ -55,7 +57,8 @@ public final class BedrockMinecraftServerPinger implements MinecraftServerPinger
if (response == null) { // No pong response if (response == null) { // No pong response
throw new ResourceNotFoundException("Server '%s' didn't respond to ping".formatted(hostname)); 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 ) { } 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));

@ -9,7 +9,9 @@ import xyz.mcutils.backend.exception.impl.BadRequestException;
import xyz.mcutils.backend.exception.impl.ResourceNotFoundException; import xyz.mcutils.backend.exception.impl.ResourceNotFoundException;
import xyz.mcutils.backend.model.dns.DNSRecord; import xyz.mcutils.backend.model.dns.DNSRecord;
import xyz.mcutils.backend.model.server.JavaMinecraftServer; 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.model.token.JavaServerStatusToken;
import xyz.mcutils.backend.service.MaxMindService;
import xyz.mcutils.backend.service.pinger.MinecraftServerPinger; import xyz.mcutils.backend.service.pinger.MinecraftServerPinger;
import java.io.DataInputStream; import java.io.DataInputStream;
@ -24,6 +26,13 @@ import java.net.*;
public final class JavaMinecraftServerPinger implements MinecraftServerPinger<JavaMinecraftServer> { public final class JavaMinecraftServerPinger implements MinecraftServerPinger<JavaMinecraftServer> {
private static final int TIMEOUT = 1500; // The timeout for the socket 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 @Override
public JavaMinecraftServer ping(String hostname, String ip, int port, DNSRecord[] records) { public JavaMinecraftServer ping(String hostname, String ip, int port, DNSRecord[] records) {
log.info("Pinging {}:{}...", hostname, port); log.info("Pinging {}:{}...", hostname, port);
@ -43,7 +52,8 @@ public final class JavaMinecraftServerPinger implements MinecraftServerPinger<Ja
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart(); JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
packetStatusInStart.process(inputStream, outputStream); packetStatusInStart.process(inputStream, outputStream);
JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class); JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class);
return JavaMinecraftServer.create(hostname, ip, port, records, token); return JavaMinecraftServer.create(hostname, ip, port, records,
MinecraftServer.GeoLocation.fromMaxMind(MaxMindService.lookup(hostname)), token);
} }
} catch (IOException ex) { } catch (IOException ex) {
if (ex instanceof UnknownHostException) { if (ex instanceof UnknownHostException) {

@ -30,6 +30,11 @@ sentry:
# The URL of the API # The URL of the API
public-url: http://localhost public-url: http://localhost
# MaxMind Configuration
# This is used for IP Geolocation
maxmind:
license: ""
# InfluxDB Configuration # InfluxDB Configuration
influx: influx:
url: http://localhost url: http://localhost