4
4
import com .google .common .hash .HashCode ;
5
5
import com .google .common .hash .HashFunction ;
6
6
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 ;
10
7
import com .maxmind .db .GeoIp2Provider ;
11
8
import com .maxmind .db .Reader ;
12
9
import com .maxmind .db .Reader .FileMode ;
16
13
import fr .xephi .authme .ConsoleLogger ;
17
14
import fr .xephi .authme .initialization .DataFolder ;
18
15
import fr .xephi .authme .output .ConsoleLoggerFactory ;
16
+ import fr .xephi .authme .settings .Settings ;
17
+ import fr .xephi .authme .settings .properties .ProtectionSettings ;
19
18
import fr .xephi .authme .util .FileUtils ;
20
19
import fr .xephi .authme .util .InternetProtocolUtils ;
21
20
22
21
import javax .inject .Inject ;
23
22
import java .io .BufferedInputStream ;
24
23
import java .io .File ;
25
- import java .io .FileNotFoundException ;
26
24
import java .io .IOException ;
27
25
import java .net .HttpURLConnection ;
28
26
import java .net .InetAddress ;
29
27
import java .net .URL ;
30
28
import java .net .UnknownHostException ;
31
- import java .nio .charset .StandardCharsets ;
32
29
import java .nio .file .Files ;
33
30
import java .nio .file .Path ;
34
31
import java .nio .file .StandardCopyOption ;
38
35
import java .time .ZoneId ;
39
36
import java .time .ZonedDateTime ;
40
37
import java .time .format .DateTimeFormatter ;
38
+ import java .util .Base64 ;
41
39
import java .util .Objects ;
42
40
import java .util .Optional ;
43
41
import java .util .zip .GZIPInputStream ;
@@ -48,39 +46,38 @@ public class GeoIpService {
48
46
"[LICENSE] This product includes GeoLite2 data created by MaxMind, available at https://www.maxmind.com" ;
49
47
50
48
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" ;
53
51
54
- private static final String ARCHIVE_FILE = DATABASE_NAME + ".tar .gz" ;
52
+ private static final String ARCHIVE_FILE = DATABASE_NAME + ".mmdb .gz" ;
55
53
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 " ;
58
56
59
57
private static final int UPDATE_INTERVAL_DAYS = 30 ;
60
58
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
-
65
59
private final ConsoleLogger logger = ConsoleLoggerFactory .get (GeoIpService .class );
66
60
private final Path dataFile ;
67
61
private final BukkitService bukkitService ;
62
+ private final Settings settings ;
68
63
69
64
private GeoIp2Provider databaseReader ;
70
65
private volatile boolean downloading ;
71
66
72
67
@ Inject
73
- GeoIpService (@ DataFolder File dataFolder , BukkitService bukkitService ) {
68
+ GeoIpService (@ DataFolder File dataFolder , BukkitService bukkitService , Settings settings ) {
74
69
this .bukkitService = bukkitService ;
75
70
this .dataFile = dataFolder .toPath ().resolve (DATABASE_FILE );
71
+ this .settings = settings ;
76
72
77
73
// Fires download of recent data or the initialization of the look up service
78
74
isDataAvailable ();
79
75
}
80
76
81
77
@ VisibleForTesting
82
- GeoIpService (@ DataFolder File dataFolder , BukkitService bukkitService , GeoIp2Provider reader ) {
78
+ GeoIpService (@ DataFolder File dataFolder , BukkitService bukkitService , Settings settings , GeoIp2Provider reader ) {
83
79
this .bukkitService = bukkitService ;
80
+ this .settings = settings ;
84
81
this .dataFile = dataFolder .toPath ().resolve (DATABASE_FILE );
85
82
86
83
this .databaseReader = reader ;
@@ -135,22 +132,26 @@ private void updateDatabase() {
135
132
logger .info ("Downloading GEO IP database, because the old database is older than "
136
133
+ UPDATE_INTERVAL_DAYS + " days or doesn't exist" );
137
134
135
+ Path downloadFile = null ;
138
136
Path tempFile = null ;
139
137
try {
140
138
// 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 ) {
143
143
logger .info ("There is no newer GEO IP database uploaded to MaxMind. Using the old one for now." );
144
144
startReading ();
145
145
return ;
146
146
}
147
147
148
+ // tar extract database and copy to target destination
149
+ extractDatabase (downloadFile , tempFile );
150
+
148
151
// MD5 checksum verification
149
- String expectedChecksum = Resources .toString (new URL (CHECKSUM_URL ), StandardCharsets .UTF_8 );
150
152
verifyChecksum (Hashing .md5 (), tempFile , expectedChecksum );
151
153
152
- // tar extract database and copy to target destination
153
- extractDatabase (tempFile , dataFile );
154
+ Files .copy (tempFile , dataFile );
154
155
155
156
//only set this value to false on success otherwise errors could lead to endless download triggers
156
157
logger .info ("Successfully downloaded new GEO IP database to " + dataFile );
@@ -159,6 +160,9 @@ private void updateDatabase() {
159
160
logger .logException ("Could not download GeoLiteAPI database" , ioEx );
160
161
} finally {
161
162
// clean up
163
+ if (downloadFile != null ) {
164
+ FileUtils .delete (downloadFile .toFile ());
165
+ }
162
166
if (tempFile != null ) {
163
167
FileUtils .delete (tempFile .toFile ());
164
168
}
@@ -178,36 +182,51 @@ private void startReading() throws IOException {
178
182
*
179
183
* @param lastModified modification timestamp of the already present file
180
184
* @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
182
186
* @throws IOException if failed during downloading and writing to destination file
183
187
*/
184
- private boolean downloadDatabaseArchive (Instant lastModified , Path destination ) throws IOException {
188
+ private String downloadDatabaseArchive (Instant lastModified , Path destination ) throws IOException {
185
189
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
+
186
201
if (lastModified != null ) {
187
202
// Only download if we actually need a newer version - this field is specified in GMT zone
188
203
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 );
190
205
connection .addRequestProperty ("If-Modified-Since" , timeFormat );
191
206
}
192
207
193
208
if (connection .getResponseCode () == HttpURLConnection .HTTP_NOT_MODIFIED ) {
194
209
//we already have the newest version
195
210
connection .getInputStream ().close ();
196
- return false ;
211
+ return null ;
197
212
}
198
213
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 ));
199
217
Files .copy (connection .getInputStream (), destination , StandardCopyOption .REPLACE_EXISTING );
200
- return true ;
218
+ Files .setLastModifiedTime (destination , FileTime .from (modifiedDate ));
219
+ return hash ;
201
220
}
202
221
203
222
/**
204
223
* Downloads the archive to the destination file if it's newer than the locally version.
205
224
*
206
225
* @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
208
227
* @throws IOException if failed during downloading and writing to destination file
209
228
*/
210
- private boolean downloadDatabaseArchive (Path destination ) throws IOException {
229
+ private String downloadDatabaseArchive (Path destination ) throws IOException {
211
230
Instant lastModified = null ;
212
231
if (Files .exists (dataFile )) {
213
232
lastModified = Files .getLastModifiedTime (dataFile ).toInstant ();
@@ -234,34 +253,23 @@ private void verifyChecksum(HashFunction function, Path file, String expectedChe
234
253
}
235
254
236
255
/**
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.
238
257
*
239
- * @param tarInputFile gzipped tar input file where the database is
258
+ * @param inputFile gzipped database input file
240
259
* @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
243
261
*/
244
- private void extractDatabase (Path tarInputFile , Path outputFile ) throws FileNotFoundException , IOException {
262
+ private void extractDatabase (Path inputFile , Path outputFile ) throws IOException {
245
263
// .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 )) {
254
266
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 );
257
269
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 ));
262
272
}
263
-
264
- throw new FileNotFoundException ("Cannot find database inside downloaded GEO IP file at " + tarInputFile );
265
273
}
266
274
267
275
/**
0 commit comments