forked from MinecraftUtilities/Backend
add location to the server response
This commit is contained in:
parent
bf992713dc
commit
3faf2d3319
3
.gitignore
vendored
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
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
116
src/main/java/xyz/mcutils/backend/service/MaxMindService.java
Normal file
116
src/main/java/xyz/mcutils/backend/service/MaxMindService.java
Normal file
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user