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
|
||||
application.yml
|
||||
target/
|
||||
|
||||
### MaxMind GeoIP2
|
||||
data/
|
||||
|
14
pom.xml
14
pom.xml
@ -165,6 +165,20 @@
|
||||
<scope>compile</scope>
|
||||
</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 -->
|
||||
<dependency>
|
||||
<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,
|
||||
@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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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)),
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
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.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));
|
||||
|
@ -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<JavaMinecraftServer> {
|
||||
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<Ja
|
||||
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
|
||||
packetStatusInStart.process(inputStream, outputStream);
|
||||
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) {
|
||||
if (ex instanceof UnknownHostException) {
|
||||
|
@ -30,6 +30,11 @@ sentry:
|
||||
# The URL of the API
|
||||
public-url: http://localhost
|
||||
|
||||
# MaxMind Configuration
|
||||
# This is used for IP Geolocation
|
||||
maxmind:
|
||||
license: ""
|
||||
|
||||
# InfluxDB Configuration
|
||||
influx:
|
||||
url: http://localhost
|
||||
|
Loading…
Reference in New Issue
Block a user