package com.earth2me.essentials.geoip; import com.earth2me.essentials.IConf; import com.earth2me.essentials.User; import com.earth2me.essentials.config.EssentialsConfiguration; import com.ice.tar.TarEntry; import com.ice.tar.TarInputStream; import com.maxmind.geoip2.DatabaseReader; import com.maxmind.geoip2.exception.AddressNotFoundException; import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.CityResponse; import com.maxmind.geoip2.model.CountryResponse; import net.ess3.api.IEssentials; import net.essentialsx.api.v2.events.AsyncUserDataLoadEvent; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.NetworkInterface; import java.net.SocketException; import java.net.URL; import java.net.URLConnection; import java.util.Arrays; import java.util.Date; import java.util.logging.Level; import java.util.zip.GZIPInputStream; import static com.earth2me.essentials.I18n.tl; public class EssentialsGeoIPPlayerListener implements Listener, IConf { private final File dataFolder; private final EssentialsConfiguration config; private final transient IEssentials ess; private final transient EssentialsGeoIP essGeo; private DatabaseReader mmreader = null; // initialize maxmind geoip2 reader private File databaseFile; EssentialsGeoIPPlayerListener(final File dataFolder, final IEssentials ess, final EssentialsGeoIP essGeo) { this.ess = ess; this.essGeo = essGeo; this.dataFolder = dataFolder; this.config = new EssentialsConfiguration(new File(dataFolder, "config.yml"), "/config.yml", EssentialsGeoIP.class); reloadConfig(); } @EventHandler(priority = EventPriority.MONITOR) public void onDataLoad(final AsyncUserDataLoadEvent event) { // Already async so just jump right into it. delayedJoin(event.getUser().getBase()); } private void delayedJoin(final Player player) { final User u = ess.getUser(player); if (u.isAuthorized("essentials.geoip.hide") || player.getAddress() == null) { return; } final InetAddress address = player.getAddress().getAddress(); final StringBuilder sb = new StringBuilder(); if (mmreader == null) { essGeo.getLogger().log(Level.WARNING, tl("geoIpErrorOnJoin", u.getName())); return; } try { if (config.getBoolean("database.show-cities", false)) { final CityResponse response = mmreader.city(address); if (response == null) { return; } final String city; final String region; final String country; city = response.getCity().getName(); region = response.getMostSpecificSubdivision().getName(); country = response.getCountry().getName(); if (city != null) { sb.append(city).append(", "); } if (region != null) { sb.append(region).append(", "); } sb.append(country); } else { final CountryResponse response = mmreader.country(address); sb.append(response.getCountry().getName()); } } catch (final AddressNotFoundException ex) { if (checkIfLocal(address)) { for (final Player online : player.getServer().getOnlinePlayers()) { final User user = ess.getUser(online); if (user.isAuthorized("essentials.geoip.show")) { user.sendMessage(tl("geoipCantFind", u.getDisplayName())); } } return; } // GeoIP2 API forced this when address not found in their DB. jar will not complied without this. // TODO: Maybe, we can set a new custom msg about addr-not-found in messages.properties. essGeo.getLogger().log(Level.INFO, tl("cantReadGeoIpDB") + " " + ex.getLocalizedMessage()); } catch (final IOException | GeoIp2Exception ex) { // GeoIP2 API forced this when address not found in their DB. jar will not complied without this. essGeo.getLogger().log(Level.SEVERE, tl("cantReadGeoIpDB") + " " + ex.getLocalizedMessage()); } if (config.getBoolean("show-on-whois", true)) { u.setGeoLocation(sb.toString()); } if (config.getBoolean("show-on-login", true) && !u.isHidden()) { for (final Player onlinePlayer : player.getServer().getOnlinePlayers()) { final User user = ess.getUser(onlinePlayer); if (user.isAuthorized("essentials.geoip.show")) { user.sendMessage(tl("geoipJoinFormat", u.getDisplayName(), sb.toString())); } } } } @Override public final void reloadConfig() { config.load(); // detect and update the old config.yml. migrate from legacy GeoIP to GeoIP2. if (!config.hasProperty("enable-locale")) { config.setProperty("database.download-url", "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key={LICENSEKEY}&suffix=tar.gz"); config.setProperty("database.download-url-city", "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key={LICENSEKEY}&suffix=tar.gz"); config.setProperty("database.license-key", ""); config.setProperty("database.update.enable", true); config.setProperty("database.update.by-every-x-days", 30); config.setProperty("enable-locale", true); config.save(); // delete old GeoIP.dat fiiles final File oldDatFile = new File(dataFolder, "GeoIP.dat"); final File oldDatFileCity = new File(dataFolder, "GeoIP-City.dat"); oldDatFile.delete(); oldDatFileCity.delete(); } if (config.getBoolean("database.show-cities", false)) { databaseFile = new File(dataFolder, "GeoIP2-City.mmdb"); } else { databaseFile = new File(dataFolder, "GeoIP2-Country.mmdb"); } if (!databaseFile.exists()) { if (config.getBoolean("database.download-if-missing", true)) { downloadDatabase(); } else { essGeo.getLogger().log(Level.SEVERE, tl("cantFindGeoIpDB")); return; } } else if (config.getBoolean("database.update.enable", true)) { // try to update expired mmdb files final long diff = new Date().getTime() - databaseFile.lastModified(); if (diff / 24 / 3600 / 1000 > config.getLong("database.update.by-every-x-days", 30)) { downloadDatabase(); } } try { // locale setting if (config.getBoolean("enable-locale", false)) { // Get geolocation based on Essentials' locale. If the locale is not avaliable, use "en". String locale = ess.getI18n().getCurrentLocale().toString().replace('_', '-'); // This fixes an inconsistency where Essentials uses "zh" but MaxMind expects "zh-CN". if ("zh".equalsIgnoreCase(locale)) { locale = "zh-CN"; } mmreader = new DatabaseReader.Builder(databaseFile).locales(Arrays.asList(locale, "en")).build(); } else { mmreader = new DatabaseReader.Builder(databaseFile).build(); } } catch (final IOException ex) { essGeo.getLogger().log(Level.SEVERE, tl("cantReadGeoIpDB"), ex); } } private void downloadDatabase() { try { String url; if (config.getBoolean("database.show-cities", false)) { url = config.getString("database.download-url-city", null); } else { url = config.getString("database.download-url", null); } if (url == null || url.isEmpty()) { essGeo.getLogger().log(Level.SEVERE, tl("geoIpUrlEmpty")); return; } final String licenseKey = config.getString("database.license-key", ""); if (licenseKey == null || licenseKey.isEmpty()) { essGeo.getLogger().log(Level.SEVERE, tl("geoIpLicenseMissing")); return; } url = url.replace("{LICENSEKEY}", licenseKey); essGeo.getLogger().log(Level.INFO, tl("downloadingGeoIp")); final URL downloadUrl = new URL(url); final URLConnection conn = downloadUrl.openConnection(); conn.setConnectTimeout(10000); conn.connect(); InputStream input = conn.getInputStream(); final OutputStream output = new FileOutputStream(databaseFile); final byte[] buffer = new byte[2048]; if (url.contains("gz")) { input = new GZIPInputStream(input); if (url.contains("tar.gz")) { // The new GeoIP2 uses tar.gz to pack the db file along with some other txt. So it makes things a bit complicated here. String filename; final TarInputStream tarInputStream = new TarInputStream(input); TarEntry entry; while ((entry = tarInputStream.getNextEntry()) != null) { if (!entry.isDirectory()) { filename = entry.getName(); if (filename.substring(filename.length() - 5).equalsIgnoreCase(".mmdb")) { input = tarInputStream; break; } } } } } int length = input.read(buffer); while (length >= 0) { output.write(buffer, 0, length); length = input.read(buffer); } output.close(); input.close(); } catch (final MalformedURLException ex) { essGeo.getLogger().log(Level.SEVERE, tl("geoIpUrlInvalid"), ex); } catch (final IOException ex) { essGeo.getLogger().log(Level.SEVERE, tl("connectionFailed"), ex); } } private boolean checkIfLocal(final InetAddress address) { if (address.isAnyLocalAddress() || address.isLoopbackAddress()) { return true; } // Double checks if address is defined on any interface try { return NetworkInterface.getByInetAddress(address) != null; } catch (final SocketException e) { return false; } } }