This commit is contained in:
parent
1392d82480
commit
f3a57dc8d9
14
API/pom.xml
14
API/pom.xml
@ -107,6 +107,12 @@
|
||||
<version>1.20-R0.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.plexus</groupId>
|
||||
<artifactId>plexus-archiver</artifactId>
|
||||
<version>4.9.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- DNS Lookup -->
|
||||
<dependency>
|
||||
@ -116,6 +122,14 @@
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- GeoIP -->
|
||||
<dependency>
|
||||
<groupId>com.maxmind.geoip2</groupId>
|
||||
<artifactId>geoip2</artifactId>
|
||||
<version>4.2.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- SwaggerUI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
|
@ -23,6 +23,11 @@
|
||||
*/
|
||||
package me.braydon.mc.model;
|
||||
|
||||
import com.maxmind.geoip2.model.CityResponse;
|
||||
import com.maxmind.geoip2.record.City;
|
||||
import com.maxmind.geoip2.record.Continent;
|
||||
import com.maxmind.geoip2.record.Country;
|
||||
import com.maxmind.geoip2.record.Location;
|
||||
import lombok.*;
|
||||
import me.braydon.mc.common.ColorUtils;
|
||||
import me.braydon.mc.model.dns.DNSRecord;
|
||||
@ -41,7 +46,7 @@ import java.util.UUID;
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
|
||||
@AllArgsConstructor @Setter @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
|
||||
public class MinecraftServer {
|
||||
/**
|
||||
* The hostname of this server.
|
||||
@ -63,6 +68,11 @@ public class MinecraftServer {
|
||||
*/
|
||||
@NonNull private final DNSRecord[] records;
|
||||
|
||||
/**
|
||||
* The Geo location of this server, null if unknown.
|
||||
*/
|
||||
private GeoLocation geo;
|
||||
|
||||
/**
|
||||
* The player counts of this server.
|
||||
*/
|
||||
@ -73,6 +83,74 @@ public class MinecraftServer {
|
||||
*/
|
||||
@NonNull private final MOTD motd;
|
||||
|
||||
/**
|
||||
* The Geo location of a server.
|
||||
*/
|
||||
@AllArgsConstructor @Getter @ToString
|
||||
public static class GeoLocation {
|
||||
/**
|
||||
* The continent of this server.
|
||||
*/
|
||||
@NonNull private final LocationData continent;
|
||||
|
||||
/**
|
||||
* The country of this server.
|
||||
*/
|
||||
@NonNull private final LocationData country;
|
||||
|
||||
/**
|
||||
* The city of this server, null if unknown.
|
||||
*/
|
||||
private final String city;
|
||||
|
||||
/**
|
||||
* The latitude of this server.
|
||||
*/
|
||||
private final double latitude;
|
||||
|
||||
/**
|
||||
* The longitude of this server.
|
||||
*/
|
||||
private final double longitude;
|
||||
|
||||
/**
|
||||
* Create new geo location data
|
||||
* from the given city response.
|
||||
*
|
||||
* @param geo the geo city response
|
||||
* @return the geo location
|
||||
*/
|
||||
@NonNull
|
||||
public static GeoLocation create(@NonNull CityResponse geo) {
|
||||
Continent continent = geo.getContinent();
|
||||
Country country = geo.getCountry();
|
||||
City city = geo.getCity();
|
||||
Location location = geo.getLocation();
|
||||
return new GeoLocation(
|
||||
new LocationData(continent.getCode(), continent.getName()),
|
||||
new LocationData(country.getIsoCode(), country.getName()),
|
||||
city == null ? null : city.getName(),
|
||||
location.getLatitude(), location.getLongitude()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for a location.
|
||||
*/
|
||||
@AllArgsConstructor @Getter @ToString
|
||||
public static class LocationData {
|
||||
/**
|
||||
* The location code.
|
||||
*/
|
||||
@NonNull private final String code;
|
||||
|
||||
/**
|
||||
* The location name.
|
||||
*/
|
||||
@NonNull private final String name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Player count data for a server.
|
||||
*/
|
||||
|
@ -54,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 DNSRecord[] records,
|
||||
@NonNull Edition edition, @NonNull Version version, @NonNull Players players, @NonNull MOTD motd,
|
||||
@NonNull GameMode gamemode) {
|
||||
super(hostname, ip, port, records, players, motd);
|
||||
private BedrockMinecraftServer(@NonNull String id, @NonNull String hostname, String ip, int port, GeoLocation geo,
|
||||
@NonNull DNSRecord[] records, @NonNull Edition edition, @NonNull Version version,
|
||||
@NonNull Players players, @NonNull MOTD motd, @NonNull GameMode gamemode) {
|
||||
super(hostname, ip, port, records, geo, players, motd);
|
||||
this.id = id;
|
||||
this.edition = edition;
|
||||
this.version = version;
|
||||
@ -83,7 +83,7 @@ public final class BedrockMinecraftServer extends MinecraftServer {
|
||||
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], split.length > 9 ? Integer.parseInt(split[9]) : -1);
|
||||
return new BedrockMinecraftServer(split[6], hostname, ip, port, records, edition, version, players, motd, gameMode);
|
||||
return new BedrockMinecraftServer(split[6], hostname, ip, port, null, records, edition, version, players, motd, gameMode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -123,11 +123,12 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
||||
*/
|
||||
private boolean mojangBanned;
|
||||
|
||||
private JavaMinecraftServer(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records, @NonNull Version version,
|
||||
@NonNull Players players, @NonNull MOTD motd, Favicon favicon, String software, Plugin[] plugins,
|
||||
ModInfo modInfo, ForgeData forgeData, String world, boolean queryEnabled, boolean previewsChat,
|
||||
boolean enforcesSecureChat, boolean preventsChatReports, boolean mojangBanned) {
|
||||
super(hostname, ip, port, records, players, motd);
|
||||
private JavaMinecraftServer(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records, GeoLocation geo,
|
||||
@NonNull Version version, @NonNull Players players, @NonNull MOTD motd, Favicon favicon,
|
||||
String software, Plugin[] plugins, ModInfo modInfo, ForgeData forgeData, String world,
|
||||
boolean queryEnabled, boolean previewsChat, boolean enforcesSecureChat, boolean preventsChatReports,
|
||||
boolean mojangBanned) {
|
||||
super(hostname, ip, port, records, geo, players, motd);
|
||||
this.version = version;
|
||||
this.favicon = favicon;
|
||||
this.software = software;
|
||||
@ -173,7 +174,7 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
||||
}
|
||||
String world = challengeStatusToken == null ? null : challengeStatusToken.getMap(); // The main server world
|
||||
|
||||
return new JavaMinecraftServer(hostname, ip, port, records, statusToken.getVersion().detailedCopy(), Players.create(statusToken.getPlayers()),
|
||||
return new JavaMinecraftServer(hostname, ip, port, records, null, statusToken.getVersion().detailedCopy(), Players.create(statusToken.getPlayers()),
|
||||
MOTD.create(motdString), Favicon.create(statusToken.getFavicon(), hostname), software, plugins, statusToken.getModInfo(),
|
||||
statusToken.getForgeData(), world, challengeStatusToken != null, statusToken.isPreviewsChat(),
|
||||
statusToken.isEnforcesSecureChat(), statusToken.isPreventsChatReports(), false
|
||||
|
@ -23,14 +23,189 @@
|
||||
*/
|
||||
package me.braydon.mc.service;
|
||||
|
||||
import com.maxmind.db.CHMCache;
|
||||
import com.maxmind.geoip2.DatabaseReader;
|
||||
import com.maxmind.geoip2.model.CityResponse;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.*;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.codehaus.plexus.archiver.tar.TarGZipUnArchiver;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@Service
|
||||
@Service @Log4j2(topic = "MaxMind")
|
||||
public final class MaxMindService {
|
||||
/**
|
||||
* The directory to store databases.
|
||||
*/
|
||||
private static final File DATABASES_DIRECTORY = new File("databases");
|
||||
|
||||
/**
|
||||
* The endpoint to download database files from.
|
||||
*/
|
||||
private static final String DATABASE_DOWNLOAD_ENDPOINT = "https://download.maxmind.com/app/geoip_download?edition_id=%s&license_key=%s&suffix=tar.gz";
|
||||
|
||||
@Value("${maxmind.license}")
|
||||
private String license;
|
||||
|
||||
/**
|
||||
* The currently loaded databases.
|
||||
*/
|
||||
private final Map<Database, DatabaseReader> databases = new HashMap<>();
|
||||
|
||||
@PostConstruct
|
||||
public void onInitialize() {
|
||||
loadDatabases(); // Load the databases
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the databases.
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void loadDatabases() {
|
||||
log.info("Loading databases...");
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
if (!DATABASES_DIRECTORY.exists()) {
|
||||
DATABASES_DIRECTORY.mkdirs();
|
||||
}
|
||||
|
||||
// Download missing databases
|
||||
for (Database database : Database.values()) {
|
||||
File databaseFile = new File(DATABASES_DIRECTORY, database.getEdition() + ".mmdb");
|
||||
if (!databaseFile.exists()) { // Doesn't exist, download it
|
||||
downloadDatabase(database, databaseFile);
|
||||
}
|
||||
// Load the database and store it
|
||||
databases.put(database, new DatabaseReader.Builder(databaseFile)
|
||||
.withCache(new CHMCache()) // Enable caching
|
||||
.build()
|
||||
);
|
||||
log.info("Loaded database {}", database.getEdition());
|
||||
}
|
||||
log.info("Loaded {} database(s)", databases.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a city by the given address.
|
||||
*
|
||||
* @param address the address
|
||||
* @return the city response
|
||||
*/
|
||||
@SneakyThrows @NonNull
|
||||
public CityResponse lookupCity(@NonNull InetAddress address) {
|
||||
return getDatabase(Database.CITY).city(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the required files
|
||||
* for the given database.
|
||||
*
|
||||
* @param database the database to download
|
||||
* @param databaseFile the file for the database
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void downloadDatabase(@NonNull Database database, @NonNull File databaseFile) {
|
||||
File downloadedFile = new File(DATABASES_DIRECTORY, database.getEdition() + ".tar.gz"); // The downloaded file
|
||||
|
||||
// Download the database if required
|
||||
if (!downloadedFile.exists()) {
|
||||
log.info("Downloading database {}...", database.getEdition());
|
||||
long before = System.currentTimeMillis();
|
||||
try (
|
||||
BufferedInputStream inputStream = new BufferedInputStream(new URL(DATABASE_DOWNLOAD_ENDPOINT.formatted(database.getEdition(), license)).openStream());
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(downloadedFile)
|
||||
) {
|
||||
byte[] dataBuffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(dataBuffer, 0, 1024)) != -1) {
|
||||
fileOutputStream.write(dataBuffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
log.info("Downloaded database {} in {}ms", database.getEdition(), System.currentTimeMillis() - before);
|
||||
}
|
||||
|
||||
// Extract the database once downloaded
|
||||
log.info("Extracting database {}...", database.getEdition());
|
||||
TarGZipUnArchiver archiver = new TarGZipUnArchiver();
|
||||
archiver.setSourceFile(downloadedFile);
|
||||
archiver.setDestDirectory(DATABASES_DIRECTORY);
|
||||
archiver.extract();
|
||||
log.info("Extracted database {}", database.getEdition());
|
||||
|
||||
// Locate the database file in the extracted directory
|
||||
File[] files = DATABASES_DIRECTORY.listFiles();
|
||||
assert files != null; // Ensure files is present
|
||||
dirLoop: for (File directory : files) {
|
||||
if (!directory.isDirectory() || !directory.getName().startsWith(database.getEdition())) {
|
||||
continue;
|
||||
}
|
||||
File[] downloadedFiles = directory.listFiles();
|
||||
assert downloadedFiles != null; // Ensures downloaded files is present
|
||||
|
||||
// Find the file for the database, move it to the
|
||||
// correct directory, and delete the downloaded contents
|
||||
for (File file : downloadedFiles) {
|
||||
if (file.isFile() && file.getName().equals(databaseFile.getName())) {
|
||||
Files.move(file.toPath(), databaseFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
// Delete the downloaded contents
|
||||
FileUtils.deleteDirectory(directory);
|
||||
FileUtils.deleteQuietly(downloadedFile);
|
||||
|
||||
break dirLoop; // We're done here
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reader for the given database.
|
||||
*
|
||||
* @param database the database to get
|
||||
* @return the database reader
|
||||
*/
|
||||
@NonNull
|
||||
public DatabaseReader getDatabase(@NonNull Database database) {
|
||||
return databases.get(database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup when the app is destroyed.
|
||||
*/
|
||||
@PreDestroy @SneakyThrows
|
||||
public void cleanup() {
|
||||
for (DatabaseReader database : databases.values()) {
|
||||
database.close();
|
||||
}
|
||||
databases.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* A database for MaxMind.
|
||||
*/
|
||||
@AllArgsConstructor @Getter @ToString
|
||||
public enum Database {
|
||||
CITY("GeoLite2-City");
|
||||
|
||||
/**
|
||||
* The edition of this database.
|
||||
*/
|
||||
@NonNull private final String edition;
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.hash.Hashing;
|
||||
import com.maxmind.geoip2.model.CityResponse;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.Getter;
|
||||
@ -65,6 +66,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -91,6 +93,11 @@ public final class MojangService {
|
||||
private static final Splitter DOT_SPLITTER = Splitter.on('.');
|
||||
private static final Joiner DOT_JOINER = Joiner.on('.');
|
||||
|
||||
/**
|
||||
* The MaxMind service to use for Geo lookups.
|
||||
*/
|
||||
@NonNull private final MaxMindService maxMindService;
|
||||
|
||||
/**
|
||||
* The cache repository for {@link Player}'s by their username.
|
||||
*/
|
||||
@ -135,8 +142,9 @@ public final class MojangService {
|
||||
private final ExpiringSet<String> blockedServersCache = new ExpiringSet<>(ExpirationPolicy.CREATED, 10L, TimeUnit.MINUTES);
|
||||
|
||||
@Autowired
|
||||
public MojangService(@NonNull PlayerNameCacheRepository playerNameCache, @NonNull PlayerCacheRepository playerCache,
|
||||
public MojangService(@NonNull MaxMindService maxMindService, @NonNull PlayerNameCacheRepository playerNameCache, @NonNull PlayerCacheRepository playerCache,
|
||||
@NonNull SkinPartTextureCacheRepository skinPartTextureCache, @NonNull MinecraftServerCacheRepository minecraftServerCache) {
|
||||
this.maxMindService = maxMindService;
|
||||
this.playerNameCache = playerNameCache;
|
||||
this.playerCache = playerCache;
|
||||
this.skinPartTextureCache = skinPartTextureCache;
|
||||
@ -454,11 +462,24 @@ public final class MojangService {
|
||||
log.info("Resolved hostname: {} -> {}", hostname, ip);
|
||||
}
|
||||
|
||||
// Attempt to perform a Geo lookup on the server
|
||||
CityResponse geo = null; // The server's Geo location
|
||||
try {
|
||||
log.info("Looking up Geo location data for {}...", ip);
|
||||
geo = maxMindService.lookupCity(InetAddress.getByName(ip)); // Get the Geo location
|
||||
} catch (Exception ex) {
|
||||
log.error("Failed looking up Geo location data for %s:".formatted(ip), ex);
|
||||
}
|
||||
|
||||
// Build our server model, cache it, and then return it
|
||||
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");
|
||||
}
|
||||
if (geo != null) { // Update Geo location data in the server if present
|
||||
response.setGeo(MinecraftServer.GeoLocation.create(geo));
|
||||
}
|
||||
|
||||
CachedMinecraftServer minecraftServer = new CachedMinecraftServer(
|
||||
platform.name() + "-" + lookupHostname, response, System.currentTimeMillis()
|
||||
);
|
||||
@ -567,6 +588,9 @@ public final class MojangService {
|
||||
return blocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup when the app is destroyed.
|
||||
*/
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
mojangServerStatuses.clear();
|
||||
|
Loading…
Reference in New Issue
Block a user