diff --git a/docs/config.md b/docs/config.md
index e032daa82..f51048bc9 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -1,5 +1,5 @@
-
+
## AuthMe Configuration
The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder,
@@ -398,6 +398,14 @@ Protection:
enableProtection: false
# Apply the protection also to registered usernames
enableProtectionRegistered: true
+ geoIpDatabase:
+ # The MaxMind clientId used to download the GeoIp database,
+ # get one at https://www.maxmind.com/en/accounts/current/license-key
+ # The EssentialsX project has a very useful tutorial on how to generate
+ # the license key: https://essentialsx.cf/wiki/GeoIP.html
+ clientId: ''
+ # The MaxMind licenseKey used to download the GeoIp database.
+ licenseKey: ''
# Countries allowed to join the server and register. For country codes, see
# https://dev.maxmind.com/geoip/legacy/codes/iso3166/
# Use "LOCALHOST" for local addresses.
@@ -576,4 +584,4 @@ To change settings on a running server, save your changes to config.yml and use
---
-This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Thu Oct 17 08:29:25 CEST 2019
+This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Tue Jan 21 10:33:12 CET 2020
diff --git a/pom.xml b/pom.xml
index 4312be4c3..7da9bda17 100644
--- a/pom.xml
+++ b/pom.xml
@@ -666,7 +666,7 @@
org.bstats
bstats-bukkit
- 1.6
+ 1.7
true
diff --git a/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java b/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java
index 7dcf137c3..93204275a 100644
--- a/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java
+++ b/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java
@@ -132,6 +132,23 @@ public class AuthMeApi {
return null;
}
+ /**
+ * Get the registration ip address of a player.
+ *
+ * @param playerName The name of the player to process
+ * @return The registration ip address of the player
+ */
+ public String getRegistrationIp(String playerName) {
+ PlayerAuth auth = playerCache.getAuth(playerName);
+ if (auth == null) {
+ auth = dataSource.getAuth(playerName);
+ }
+ if (auth != null) {
+ return auth.getRegistrationIp();
+ }
+ return null;
+ }
+
/**
* Get the last ip address of a player.
*
@@ -196,6 +213,29 @@ public class AuthMeApi {
return null;
}
+ /**
+ * Get the registration (AuthMe) timestamp of a player.
+ *
+ * @param playerName The name of the player to process
+ *
+ * @return The timestamp of when the player was registered, or null if the player doesn't exist or is not registered
+ */
+ public Instant getRegistrationTime(String playerName) {
+ Long registrationDate = getRegistrationMillis(playerName);
+ return registrationDate == null ? null : Instant.ofEpochMilli(registrationDate);
+ }
+
+ private Long getRegistrationMillis(String playerName) {
+ PlayerAuth auth = playerCache.getAuth(playerName);
+ if (auth == null) {
+ auth = dataSource.getAuth(playerName);
+ }
+ if (auth != null) {
+ return auth.getRegistrationDate();
+ }
+ return null;
+ }
+
/**
* Return whether the player is registered.
*
diff --git a/src/main/java/fr/xephi/authme/initialization/OnStartupTasks.java b/src/main/java/fr/xephi/authme/initialization/OnStartupTasks.java
index e1108156e..93cdd41bd 100644
--- a/src/main/java/fr/xephi/authme/initialization/OnStartupTasks.java
+++ b/src/main/java/fr/xephi/authme/initialization/OnStartupTasks.java
@@ -51,7 +51,7 @@ public class OnStartupTasks {
* @param settings the settings
*/
public static void sendMetrics(AuthMe plugin, Settings settings) {
- final Metrics metrics = new Metrics(plugin);
+ final Metrics metrics = new Metrics(plugin, 164);
metrics.addCustomChart(new Metrics.SimplePie("messages_language",
() -> settings.getProperty(PluginSettings.MESSAGES_LANGUAGE)));
diff --git a/src/main/java/fr/xephi/authme/service/GeoIpService.java b/src/main/java/fr/xephi/authme/service/GeoIpService.java
index b4f900955..59a62d8b5 100644
--- a/src/main/java/fr/xephi/authme/service/GeoIpService.java
+++ b/src/main/java/fr/xephi/authme/service/GeoIpService.java
@@ -4,9 +4,6 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
-import com.google.common.io.Resources;
-import com.ice.tar.TarEntry;
-import com.ice.tar.TarInputStream;
import com.maxmind.db.GeoIp2Provider;
import com.maxmind.db.Reader;
import com.maxmind.db.Reader.FileMode;
@@ -18,19 +15,19 @@ import fr.xephi.authme.ThreadSafetyUtils;
import fr.xephi.authme.annotation.ShouldBeAsync;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.output.ConsoleLoggerFactory;
+import fr.xephi.authme.settings.Settings;
+import fr.xephi.authme.settings.properties.ProtectionSettings;
import fr.xephi.authme.util.FileUtils;
import fr.xephi.authme.util.InternetProtocolUtils;
import javax.inject.Inject;
import java.io.BufferedInputStream;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
-import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
@@ -40,6 +37,7 @@ import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
+import java.util.Base64;
import java.util.Objects;
import java.util.Optional;
import java.util.zip.GZIPInputStream;
@@ -50,39 +48,38 @@ public class GeoIpService {
"[LICENSE] This product includes GeoLite2 data created by MaxMind, available at https://www.maxmind.com";
private static final String DATABASE_NAME = "GeoLite2-Country";
- private static final String DATABASE_EXT = ".mmdb";
- private static final String DATABASE_FILE = DATABASE_NAME + DATABASE_EXT;
+ private static final String DATABASE_FILE = DATABASE_NAME + ".mmdb";
+ private static final String DATABASE_TMP_FILE = DATABASE_NAME + ".mmdb.tmp";
- private static final String ARCHIVE_FILE = DATABASE_NAME + ".tar.gz";
+ private static final String ARCHIVE_FILE = DATABASE_NAME + ".mmdb.gz";
- private static final String ARCHIVE_URL = "https://geolite.maxmind.com/download/geoip/database/" + ARCHIVE_FILE;
- private static final String CHECKSUM_URL = ARCHIVE_URL + ".md5";
+ private static final String ARCHIVE_URL =
+ "https://updates.maxmind.com/geoip/databases/" + DATABASE_NAME + "/update";
private static final int UPDATE_INTERVAL_DAYS = 30;
- // The server for MaxMind doesn't seem to understand RFC1123,
- // but every HTTP implementation have to support RFC 1023
- private static final String TIME_RFC_1023 = "EEE, dd-MMM-yy HH:mm:ss zzz";
-
private final ConsoleLogger logger = ConsoleLoggerFactory.get(GeoIpService.class);
private final Path dataFile;
private final BukkitService bukkitService;
+ private final Settings settings;
private GeoIp2Provider databaseReader;
private volatile boolean downloading;
@Inject
- GeoIpService(@DataFolder File dataFolder, BukkitService bukkitService) {
+ GeoIpService(@DataFolder File dataFolder, BukkitService bukkitService, Settings settings) {
this.bukkitService = bukkitService;
this.dataFile = dataFolder.toPath().resolve(DATABASE_FILE);
+ this.settings = settings;
// Fires download of recent data or the initialization of the look up service
isDataAvailable();
}
@VisibleForTesting
- GeoIpService(@DataFolder File dataFolder, BukkitService bukkitService, GeoIp2Provider reader) {
+ GeoIpService(@DataFolder File dataFolder, BukkitService bukkitService, Settings settings, GeoIp2Provider reader) {
this.bukkitService = bukkitService;
+ this.settings = settings;
this.dataFile = dataFolder.toPath().resolve(DATABASE_FILE);
this.databaseReader = reader;
@@ -139,22 +136,26 @@ public class GeoIpService {
logger.info("Downloading GEO IP database, because the old database is older than "
+ UPDATE_INTERVAL_DAYS + " days or doesn't exist");
+ Path downloadFile = null;
Path tempFile = null;
try {
// download database to temporarily location
- tempFile = Files.createTempFile(ARCHIVE_FILE, null);
- if (!downloadDatabaseArchive(tempFile)) {
+ downloadFile = Files.createTempFile(ARCHIVE_FILE, null);
+ tempFile = Files.createTempFile(DATABASE_TMP_FILE, null);
+ String expectedChecksum = downloadDatabaseArchive(downloadFile);
+ if (expectedChecksum == null) {
logger.info("There is no newer GEO IP database uploaded to MaxMind. Using the old one for now.");
startReading();
return;
}
+ // tar extract database and copy to target destination
+ extractDatabase(downloadFile, tempFile);
+
// MD5 checksum verification
- String expectedChecksum = Resources.toString(new URL(CHECKSUM_URL), StandardCharsets.UTF_8);
verifyChecksum(Hashing.md5(), tempFile, expectedChecksum);
- // tar extract database and copy to target destination
- extractDatabase(tempFile, dataFile);
+ Files.copy(tempFile, dataFile, StandardCopyOption.REPLACE_EXISTING);
//only set this value to false on success otherwise errors could lead to endless download triggers
logger.info("Successfully downloaded new GEO IP database to " + dataFile);
@@ -163,6 +164,9 @@ public class GeoIpService {
logger.logException("Could not download GeoLiteAPI database", ioEx);
} finally {
// clean up
+ if (downloadFile != null) {
+ FileUtils.delete(downloadFile.toFile());
+ }
if (tempFile != null) {
FileUtils.delete(tempFile.toFile());
}
@@ -182,36 +186,51 @@ public class GeoIpService {
*
* @param lastModified modification timestamp of the already present file
* @param destination save file
- * @return false if we already have the newest version, true if successful
+ * @return null if no updates were found, the MD5 hash of the downloaded archive if successful
* @throws IOException if failed during downloading and writing to destination file
*/
- private boolean downloadDatabaseArchive(Instant lastModified, Path destination) throws IOException {
+ private String downloadDatabaseArchive(Instant lastModified, Path destination) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(ARCHIVE_URL).openConnection();
+
+ String clientId = settings.getProperty(ProtectionSettings.MAXMIND_API_CLIENT_ID);
+ String licenseKey = settings.getProperty(ProtectionSettings.MAXMIND_API_LICENSE_KEY);
+ if (clientId.isEmpty() || licenseKey.isEmpty()) {
+ logger.warning("No MaxMind credentials found in the configuration file!"
+ + " GeoIp protections will be disabled.");
+ return null;
+ }
+ String basicAuth = "Basic " + new String(Base64.getEncoder().encode((clientId + ":" + licenseKey).getBytes()));
+ connection.setRequestProperty("Authorization", basicAuth);
+
if (lastModified != null) {
// Only download if we actually need a newer version - this field is specified in GMT zone
ZonedDateTime zonedTime = lastModified.atZone(ZoneId.of("GMT"));
- String timeFormat = DateTimeFormatter.ofPattern(TIME_RFC_1023).format(zonedTime);
+ String timeFormat = DateTimeFormatter.RFC_1123_DATE_TIME.format(zonedTime);
connection.addRequestProperty("If-Modified-Since", timeFormat);
}
if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
//we already have the newest version
connection.getInputStream().close();
- return false;
+ return null;
}
+ String hash = connection.getHeaderField("X-Database-MD5");
+ String rawModifiedDate = connection.getHeaderField("Last-Modified");
+ Instant modifiedDate = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(rawModifiedDate));
Files.copy(connection.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
- return true;
+ Files.setLastModifiedTime(destination, FileTime.from(modifiedDate));
+ return hash;
}
/**
* Downloads the archive to the destination file if it's newer than the locally version.
*
* @param destination save file
- * @return false if we already have the newest version, true if successful
+ * @return null if no updates were found, the MD5 hash of the downloaded archive if successful
* @throws IOException if failed during downloading and writing to destination file
*/
- private boolean downloadDatabaseArchive(Path destination) throws IOException {
+ private String downloadDatabaseArchive(Path destination) throws IOException {
Instant lastModified = null;
if (Files.exists(dataFile)) {
lastModified = Files.getLastModifiedTime(dataFile).toInstant();
@@ -238,34 +257,23 @@ public class GeoIpService {
}
/**
- * Extract the database from the tar archive. Existing outputFile will be replaced if it already exists.
+ * Extract the database from gzipped data. Existing outputFile will be replaced if it already exists.
*
- * @param tarInputFile gzipped tar input file where the database is
+ * @param inputFile gzipped database input file
* @param outputFile destination file for the database
- * @throws IOException on I/O error reading the tar archive, or writing the output
- * @throws FileNotFoundException if the database cannot be found inside the archive
+ * @throws IOException on I/O error reading the archive, or writing the output
*/
- private void extractDatabase(Path tarInputFile, Path outputFile) throws FileNotFoundException, IOException {
+ private void extractDatabase(Path inputFile, Path outputFile) throws IOException {
// .gz -> gzipped file
- try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(tarInputFile));
- TarInputStream tarIn = new TarInputStream(new GZIPInputStream(in))) {
- for (TarEntry entry = tarIn.getNextEntry(); entry != null; entry = tarIn.getNextEntry()) {
- // filename including folders (absolute path inside the archive)
- String filename = entry.getName();
- if (entry.isDirectory() || !filename.endsWith(DATABASE_EXT)) {
- continue;
- }
+ try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(inputFile));
+ GZIPInputStream gzipIn = new GZIPInputStream(in)) {
- // found the database file and copy file
- Files.copy(tarIn, outputFile, StandardCopyOption.REPLACE_EXISTING);
+ // found the database file and copy file
+ Files.copy(gzipIn, outputFile, StandardCopyOption.REPLACE_EXISTING);
- // update the last modification date to be same as in the archive
- Files.setLastModifiedTime(outputFile, FileTime.from(entry.getModTime().toInstant()));
- return;
- }
+ // update the last modification date to be same as in the archive
+ Files.setLastModifiedTime(outputFile, Files.getLastModifiedTime(inputFile));
}
-
- throw new FileNotFoundException("Cannot find database inside downloaded GEO IP file at " + tarInputFile);
}
/**
diff --git a/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java b/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java
index ba724afa9..6f5730842 100644
--- a/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java
+++ b/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java
@@ -20,6 +20,17 @@ public final class ProtectionSettings implements SettingsHolder {
public static final Property ENABLE_PROTECTION_REGISTERED =
newProperty("Protection.enableProtectionRegistered", true);
+ @Comment({"The MaxMind clientId used to download the GeoIp database,",
+ "get one at https://www.maxmind.com/en/accounts/current/license-key",
+ "The EssentialsX project has a very useful tutorial on how to generate",
+ "the license key: https://essentialsx.cf/wiki/GeoIP.html"})
+ public static final Property MAXMIND_API_CLIENT_ID =
+ newProperty("Protection.geoIpDatabase.clientId", "");
+
+ @Comment("The MaxMind licenseKey used to download the GeoIp database.")
+ public static final Property MAXMIND_API_LICENSE_KEY =
+ newProperty("Protection.geoIpDatabase.licenseKey", "");
+
@Comment({
"Countries allowed to join the server and register. For country codes, see",
"https://dev.maxmind.com/geoip/legacy/codes/iso3166/",
diff --git a/src/test/java/fr/xephi/authme/service/GeoIpServiceTest.java b/src/test/java/fr/xephi/authme/service/GeoIpServiceTest.java
index 29220808b..8654ec8b5 100644
--- a/src/test/java/fr/xephi/authme/service/GeoIpServiceTest.java
+++ b/src/test/java/fr/xephi/authme/service/GeoIpServiceTest.java
@@ -8,6 +8,7 @@ import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
+import fr.xephi.authme.settings.Settings;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -40,13 +41,16 @@ public class GeoIpServiceTest {
@Mock
private BukkitService bukkitService;
+ @Mock
+ private Settings settings;
+
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Before
public void initializeGeoLiteApi() throws IOException {
dataFolder = temporaryFolder.newFolder();
- geoIpService = new GeoIpService(dataFolder, bukkitService, lookupService);
+ geoIpService = new GeoIpService(dataFolder, bukkitService, settings, lookupService);
}
@Test