Skip to content

Commit a43127d

Browse files
authored
Update the GeoIp database download method [BREAKING] (#1990)
* Update the GeoIp database download method [BREAKING] Now GeoIp database updates require a ClientID and a LicenseKey, which can be obtained for free at https://www.maxmind.com/en/accounts/current/license-key * Codestyle
1 parent 04865ae commit a43127d

File tree

4 files changed

+79
-52
lines changed

4 files changed

+79
-52
lines changed

docs/config.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
2-
<!-- File auto-generated on Thu Oct 17 08:29:25 CEST 2019. See docs/config/config.tpl.md -->
2+
<!-- File auto-generated on Mon Jan 20 14:16:50 CET 2020. See docs/config/config.tpl.md -->
33

44
## AuthMe Configuration
55
The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder,
@@ -398,6 +398,12 @@ Protection:
398398
enableProtection: false
399399
# Apply the protection also to registered usernames
400400
enableProtectionRegistered: true
401+
geoIpDatabase:
402+
# The MaxMind clientId used to download the GeoIp database,
403+
# get one at https://www.maxmind.com/en/accounts/current/license-key
404+
clientId: ''
405+
# The MaxMind licenseKey used to download the GeoIp database.
406+
licenseKey: ''
401407
# Countries allowed to join the server and register. For country codes, see
402408
# https://dev.maxmind.com/geoip/legacy/codes/iso3166/
403409
# Use "LOCALHOST" for local addresses.
@@ -576,4 +582,4 @@ To change settings on a running server, save your changes to config.yml and use
576582

577583
---
578584

579-
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
585+
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon Jan 20 14:16:50 CET 2020

src/main/java/fr/xephi/authme/service/GeoIpService.java

+57-49
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
import com.google.common.hash.HashCode;
55
import com.google.common.hash.HashFunction;
66
import com.google.common.hash.Hashing;
7-
import com.google.common.io.Resources;
8-
import com.ice.tar.TarEntry;
9-
import com.ice.tar.TarInputStream;
107
import com.maxmind.db.GeoIp2Provider;
118
import com.maxmind.db.Reader;
129
import com.maxmind.db.Reader.FileMode;
@@ -16,19 +13,19 @@
1613
import fr.xephi.authme.ConsoleLogger;
1714
import fr.xephi.authme.initialization.DataFolder;
1815
import fr.xephi.authme.output.ConsoleLoggerFactory;
16+
import fr.xephi.authme.settings.Settings;
17+
import fr.xephi.authme.settings.properties.ProtectionSettings;
1918
import fr.xephi.authme.util.FileUtils;
2019
import fr.xephi.authme.util.InternetProtocolUtils;
2120

2221
import javax.inject.Inject;
2322
import java.io.BufferedInputStream;
2423
import java.io.File;
25-
import java.io.FileNotFoundException;
2624
import java.io.IOException;
2725
import java.net.HttpURLConnection;
2826
import java.net.InetAddress;
2927
import java.net.URL;
3028
import java.net.UnknownHostException;
31-
import java.nio.charset.StandardCharsets;
3229
import java.nio.file.Files;
3330
import java.nio.file.Path;
3431
import java.nio.file.StandardCopyOption;
@@ -38,6 +35,7 @@
3835
import java.time.ZoneId;
3936
import java.time.ZonedDateTime;
4037
import java.time.format.DateTimeFormatter;
38+
import java.util.Base64;
4139
import java.util.Objects;
4240
import java.util.Optional;
4341
import java.util.zip.GZIPInputStream;
@@ -48,39 +46,38 @@ public class GeoIpService {
4846
"[LICENSE] This product includes GeoLite2 data created by MaxMind, available at https://www.maxmind.com";
4947

5048
private static final String DATABASE_NAME = "GeoLite2-Country";
51-
private static final String DATABASE_EXT = ".mmdb";
52-
private static final String DATABASE_FILE = DATABASE_NAME + DATABASE_EXT;
49+
private static final String DATABASE_FILE = DATABASE_NAME + ".mmdb";
50+
private static final String DATABASE_TMP_FILE = DATABASE_NAME + ".mmdb.tmp";
5351

54-
private static final String ARCHIVE_FILE = DATABASE_NAME + ".tar.gz";
52+
private static final String ARCHIVE_FILE = DATABASE_NAME + ".mmdb.gz";
5553

56-
private static final String ARCHIVE_URL = "https://geolite.maxmind.com/download/geoip/database/" + ARCHIVE_FILE;
57-
private static final String CHECKSUM_URL = ARCHIVE_URL + ".md5";
54+
private static final String ARCHIVE_URL =
55+
"https://updates.maxmind.com/geoip/databases/" + DATABASE_NAME + "/update";
5856

5957
private static final int UPDATE_INTERVAL_DAYS = 30;
6058

61-
// The server for MaxMind doesn't seem to understand RFC1123,
62-
// but every HTTP implementation have to support RFC 1023
63-
private static final String TIME_RFC_1023 = "EEE, dd-MMM-yy HH:mm:ss zzz";
64-
6559
private final ConsoleLogger logger = ConsoleLoggerFactory.get(GeoIpService.class);
6660
private final Path dataFile;
6761
private final BukkitService bukkitService;
62+
private final Settings settings;
6863

6964
private GeoIp2Provider databaseReader;
7065
private volatile boolean downloading;
7166

7267
@Inject
73-
GeoIpService(@DataFolder File dataFolder, BukkitService bukkitService) {
68+
GeoIpService(@DataFolder File dataFolder, BukkitService bukkitService, Settings settings) {
7469
this.bukkitService = bukkitService;
7570
this.dataFile = dataFolder.toPath().resolve(DATABASE_FILE);
71+
this.settings = settings;
7672

7773
// Fires download of recent data or the initialization of the look up service
7874
isDataAvailable();
7975
}
8076

8177
@VisibleForTesting
82-
GeoIpService(@DataFolder File dataFolder, BukkitService bukkitService, GeoIp2Provider reader) {
78+
GeoIpService(@DataFolder File dataFolder, BukkitService bukkitService, Settings settings, GeoIp2Provider reader) {
8379
this.bukkitService = bukkitService;
80+
this.settings = settings;
8481
this.dataFile = dataFolder.toPath().resolve(DATABASE_FILE);
8582

8683
this.databaseReader = reader;
@@ -135,22 +132,26 @@ private void updateDatabase() {
135132
logger.info("Downloading GEO IP database, because the old database is older than "
136133
+ UPDATE_INTERVAL_DAYS + " days or doesn't exist");
137134

135+
Path downloadFile = null;
138136
Path tempFile = null;
139137
try {
140138
// download database to temporarily location
141-
tempFile = Files.createTempFile(ARCHIVE_FILE, null);
142-
if (!downloadDatabaseArchive(tempFile)) {
139+
downloadFile = Files.createTempFile(ARCHIVE_FILE, null);
140+
tempFile = Files.createTempFile(DATABASE_TMP_FILE, null);
141+
String expectedChecksum = downloadDatabaseArchive(downloadFile);
142+
if (expectedChecksum == null) {
143143
logger.info("There is no newer GEO IP database uploaded to MaxMind. Using the old one for now.");
144144
startReading();
145145
return;
146146
}
147147

148+
// tar extract database and copy to target destination
149+
extractDatabase(downloadFile, tempFile);
150+
148151
// MD5 checksum verification
149-
String expectedChecksum = Resources.toString(new URL(CHECKSUM_URL), StandardCharsets.UTF_8);
150152
verifyChecksum(Hashing.md5(), tempFile, expectedChecksum);
151153

152-
// tar extract database and copy to target destination
153-
extractDatabase(tempFile, dataFile);
154+
Files.copy(tempFile, dataFile);
154155

155156
//only set this value to false on success otherwise errors could lead to endless download triggers
156157
logger.info("Successfully downloaded new GEO IP database to " + dataFile);
@@ -159,6 +160,9 @@ private void updateDatabase() {
159160
logger.logException("Could not download GeoLiteAPI database", ioEx);
160161
} finally {
161162
// clean up
163+
if (downloadFile != null) {
164+
FileUtils.delete(downloadFile.toFile());
165+
}
162166
if (tempFile != null) {
163167
FileUtils.delete(tempFile.toFile());
164168
}
@@ -178,36 +182,51 @@ private void startReading() throws IOException {
178182
*
179183
* @param lastModified modification timestamp of the already present file
180184
* @param destination save file
181-
* @return false if we already have the newest version, true if successful
185+
* @return null if no updates were found, the MD5 hash of the downloaded archive if successful
182186
* @throws IOException if failed during downloading and writing to destination file
183187
*/
184-
private boolean downloadDatabaseArchive(Instant lastModified, Path destination) throws IOException {
188+
private String downloadDatabaseArchive(Instant lastModified, Path destination) throws IOException {
185189
HttpURLConnection connection = (HttpURLConnection) new URL(ARCHIVE_URL).openConnection();
190+
191+
String clientId = settings.getProperty(ProtectionSettings.MAXMIND_API_CLIENT_ID);
192+
String licenseKey = settings.getProperty(ProtectionSettings.MAXMIND_API_LICENSE_KEY);
193+
if (clientId.isEmpty() || licenseKey.isEmpty()) {
194+
logger.warning("No MaxMind credentials found in the configuration file!"
195+
+ " GeoIp protections will be disabled.");
196+
return null;
197+
}
198+
String basicAuth = "Basic " + new String(Base64.getEncoder().encode((clientId + ":" + licenseKey).getBytes()));
199+
connection.setRequestProperty("Authorization", basicAuth);
200+
186201
if (lastModified != null) {
187202
// Only download if we actually need a newer version - this field is specified in GMT zone
188203
ZonedDateTime zonedTime = lastModified.atZone(ZoneId.of("GMT"));
189-
String timeFormat = DateTimeFormatter.ofPattern(TIME_RFC_1023).format(zonedTime);
204+
String timeFormat = DateTimeFormatter.RFC_1123_DATE_TIME.format(zonedTime);
190205
connection.addRequestProperty("If-Modified-Since", timeFormat);
191206
}
192207

193208
if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
194209
//we already have the newest version
195210
connection.getInputStream().close();
196-
return false;
211+
return null;
197212
}
198213

214+
String hash = connection.getHeaderField("X-Database-MD5");
215+
String rawModifiedDate = connection.getHeaderField("Last-Modified");
216+
Instant modifiedDate = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(rawModifiedDate));
199217
Files.copy(connection.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
200-
return true;
218+
Files.setLastModifiedTime(destination, FileTime.from(modifiedDate));
219+
return hash;
201220
}
202221

203222
/**
204223
* Downloads the archive to the destination file if it's newer than the locally version.
205224
*
206225
* @param destination save file
207-
* @return false if we already have the newest version, true if successful
226+
* @return null if no updates were found, the MD5 hash of the downloaded archive if successful
208227
* @throws IOException if failed during downloading and writing to destination file
209228
*/
210-
private boolean downloadDatabaseArchive(Path destination) throws IOException {
229+
private String downloadDatabaseArchive(Path destination) throws IOException {
211230
Instant lastModified = null;
212231
if (Files.exists(dataFile)) {
213232
lastModified = Files.getLastModifiedTime(dataFile).toInstant();
@@ -234,34 +253,23 @@ private void verifyChecksum(HashFunction function, Path file, String expectedChe
234253
}
235254

236255
/**
237-
* Extract the database from the tar archive. Existing outputFile will be replaced if it already exists.
256+
* Extract the database from gzipped data. Existing outputFile will be replaced if it already exists.
238257
*
239-
* @param tarInputFile gzipped tar input file where the database is
258+
* @param inputFile gzipped database input file
240259
* @param outputFile destination file for the database
241-
* @throws IOException on I/O error reading the tar archive, or writing the output
242-
* @throws FileNotFoundException if the database cannot be found inside the archive
260+
* @throws IOException on I/O error reading the archive, or writing the output
243261
*/
244-
private void extractDatabase(Path tarInputFile, Path outputFile) throws FileNotFoundException, IOException {
262+
private void extractDatabase(Path inputFile, Path outputFile) throws IOException {
245263
// .gz -> gzipped file
246-
try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(tarInputFile));
247-
TarInputStream tarIn = new TarInputStream(new GZIPInputStream(in))) {
248-
for (TarEntry entry = tarIn.getNextEntry(); entry != null; entry = tarIn.getNextEntry()) {
249-
// filename including folders (absolute path inside the archive)
250-
String filename = entry.getName();
251-
if (entry.isDirectory() || !filename.endsWith(DATABASE_EXT)) {
252-
continue;
253-
}
264+
try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(inputFile));
265+
GZIPInputStream gzipIn = new GZIPInputStream(in)) {
254266

255-
// found the database file and copy file
256-
Files.copy(tarIn, outputFile, StandardCopyOption.REPLACE_EXISTING);
267+
// found the database file and copy file
268+
Files.copy(gzipIn, outputFile, StandardCopyOption.REPLACE_EXISTING);
257269

258-
// update the last modification date to be same as in the archive
259-
Files.setLastModifiedTime(outputFile, FileTime.from(entry.getModTime().toInstant()));
260-
return;
261-
}
270+
// update the last modification date to be same as in the archive
271+
Files.setLastModifiedTime(outputFile, Files.getLastModifiedTime(inputFile));
262272
}
263-
264-
throw new FileNotFoundException("Cannot find database inside downloaded GEO IP file at " + tarInputFile);
265273
}
266274

267275
/**

src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java

+9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ public final class ProtectionSettings implements SettingsHolder {
2020
public static final Property<Boolean> ENABLE_PROTECTION_REGISTERED =
2121
newProperty("Protection.enableProtectionRegistered", true);
2222

23+
@Comment({"The MaxMind clientId used to download the GeoIp database,",
24+
"get one at https://www.maxmind.com/en/accounts/current/license-key"})
25+
public static final Property<String> MAXMIND_API_CLIENT_ID =
26+
newProperty("Protection.geoIpDatabase.clientId", "");
27+
28+
@Comment("The MaxMind licenseKey used to download the GeoIp database.")
29+
public static final Property<String> MAXMIND_API_LICENSE_KEY =
30+
newProperty("Protection.geoIpDatabase.licenseKey", "");
31+
2332
@Comment({
2433
"Countries allowed to join the server and register. For country codes, see",
2534
"https://dev.maxmind.com/geoip/legacy/codes/iso3166/",

src/test/java/fr/xephi/authme/service/GeoIpServiceTest.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.io.IOException;
99
import java.net.InetAddress;
1010

11+
import fr.xephi.authme.settings.Settings;
1112
import org.junit.Before;
1213
import org.junit.Rule;
1314
import org.junit.Test;
@@ -40,13 +41,16 @@ public class GeoIpServiceTest {
4041
@Mock
4142
private BukkitService bukkitService;
4243

44+
@Mock
45+
private Settings settings;
46+
4347
@Rule
4448
public TemporaryFolder temporaryFolder = new TemporaryFolder();
4549

4650
@Before
4751
public void initializeGeoLiteApi() throws IOException {
4852
dataFolder = temporaryFolder.newFolder();
49-
geoIpService = new GeoIpService(dataFolder, bukkitService, lookupService);
53+
geoIpService = new GeoIpService(dataFolder, bukkitService, settings, lookupService);
5054
}
5155

5256
@Test

0 commit comments

Comments
 (0)