From 2cb863356c639fab937c7907f501e7e3bbbe6e6d Mon Sep 17 00:00:00 2001 From: wode490390 Date: Wed, 14 Aug 2019 13:48:53 +0800 Subject: [PATCH] 1.0.0 --- README.md | 48 ++- pom.xml | 86 ++++ .../cn/wode490390/nukkit/geoip/GeoIP.java | 144 +++++++ .../wode490390/nukkit/geoip/GeoIPCommand.java | 60 +++ .../nukkit/geoip/GeoIPListener.java | 116 ++++++ .../wode490390/nukkit/geoip/MetricsLite.java | 366 ++++++++++++++++++ src/main/resources/config.yml | 15 + src/main/resources/plugin.yml | 18 + 8 files changed, 852 insertions(+), 1 deletion(-) create mode 100644 pom.xml create mode 100644 src/main/java/cn/wode490390/nukkit/geoip/GeoIP.java create mode 100644 src/main/java/cn/wode490390/nukkit/geoip/GeoIPCommand.java create mode 100644 src/main/java/cn/wode490390/nukkit/geoip/GeoIPListener.java create mode 100644 src/main/java/cn/wode490390/nukkit/geoip/MetricsLite.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/plugin.yml diff --git a/README.md b/README.md index 7ba710b..1fead45 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,48 @@ # GeoIP -GeoIP plugin for Nukkit. GeoIP provides an approximate lookup of where your players come from, based on their public IP and public geographical databases. +[![](https://i.loli.net/2019/08/11/g9PU5ufFoqmeKjp.png)](http://www.mcbbs.net/thread-900823-1-1.html "IP定位") + +GeoIP plugin for Nukkit. + +GeoIP provides an approximate lookup of where your players come from, based on their public IP and public geographical databases. + +Please see [mcbbs](http://www.mcbbs.net/thread-900823-1-1.html) for more information. +## Permissions +| Command | Permission | Description | Default | +| - | - | - | - | +| `/geoip` | geoip.show | Shows the GeoIP location of a player. | OP | +| `/geoip` | geoip.show.fullip | Shows the full ip address of a player. | false | +| | geoip.hide | Allows player to hide player's country and city from people who have permission geoip.show | false | +## config.yml +```yaml +database: + show-cities: false + download-if-missing: true + # Url for country + download-url: "https://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz" + # Url for cities + download-url-city: "https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz" + update: + enable: true + by-every-x-days: 30 +show-on-login: true +# "enable-locale" enables locale on geolocation display. +enable-locale: true +# Not all languages are supported. See https://dev.maxmind.com/geoip/geoip2/web-services/#Languages +locale: en +``` +## API Usage +```java +import cn.nukkit.Player; +import cn.nukkit.Server; +import cn.wode490390.nukkit.geoip.GeoIP; + +class Example { + Example() { + Player player = Server.getInstance().getPlayer("wode490390"); + if (player != null) { + String geoLocation = GeoIP.query(player); //Our API :) + player.sendMessage("Your location: " + geoLocation); + } + } +} +``` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ccedd43 --- /dev/null +++ b/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + cn.wode490390.nukkit + geoip + 1.0.0 + GeoIP + GeoIP plugin for Nukkit + 2018 + http://wode490390.cn/ + jar + + + GNU General Public License, Version 3.0 + http://www.gnu.org/licenses/gpl.html + repo + + + + 1.8 + 1.8 + UTF-8 + + + + nukkitx + http://repo.nukkitx.com/main/ + + + + + cn.nukkit + nukkit + 1.0-SNAPSHOT + provided + + + com.maxmind.geoip2 + geoip2 + 2.12.0 + + + javatar + javatar + 2.5 + + + + wodeGeoIP-${project.version} + clean package + + + . + true + ${basedir}/src/main/resources + + *.yml + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + + + false + true + + + *:* + + + + + + + diff --git a/src/main/java/cn/wode490390/nukkit/geoip/GeoIP.java b/src/main/java/cn/wode490390/nukkit/geoip/GeoIP.java new file mode 100644 index 0000000..301327c --- /dev/null +++ b/src/main/java/cn/wode490390/nukkit/geoip/GeoIP.java @@ -0,0 +1,144 @@ +package cn.wode490390.nukkit.geoip; + +import cn.nukkit.Player; +import cn.nukkit.plugin.PluginBase; +import cn.nukkit.utils.Config; +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import com.ice.tar.TarEntry; +import com.ice.tar.TarInputStream; +import com.maxmind.geoip2.DatabaseReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.Date; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +public class GeoIP extends PluginBase { + + private static final Map cache = Maps.newHashMap(); + + /** + * Querys player's geographical location. + * + * @param palyer + * @return geographical location or null + */ + public static String query(Player palyer) { + Preconditions.checkNotNull(palyer, "Player cannot be null"); + return cache.get(palyer); + } + + static void setGeoLocation(Player palyer, String location) { + cache.put(palyer, location); + } + + Config config; + private File databaseFile; + + @Override + public void onEnable() { + try { + new MetricsLite(this); + } catch (Exception ignore) { + + } + this.saveDefaultConfig(); + this.config = this.getConfig(); + if (this.config.getBoolean("database.show-cities", false)) { + this.databaseFile = new File(this.getDataFolder(), "GeoIP2-City.mmdb"); + } else { + this.databaseFile = new File(this.getDataFolder(), "GeoIP2-Country.mmdb"); + } + if (!this.databaseFile.exists()) { + if (this.config.getBoolean("database.download-if-missing", true)) { + this.downloadDatabase(); + } else { + this.getLogger().warning("Can't find GeoIP database!"); + this.setEnabled(false); + return; + } + } else if (this.config.getBoolean("database.update.enable", true)) { + // try to update expired mmdb files + long diff = new Date().getTime() - this.databaseFile.lastModified(); + if (diff / 86400000 > this.config.getLong("database.update.by-every-x-days", 30)) { + this.downloadDatabase(); + } + } + DatabaseReader mmreader; + try { + // locale setting + if (this.config.getBoolean("enable-locale")) { + // If the locale is not avaliable, use "en". + mmreader = new DatabaseReader.Builder(this.databaseFile).locales(Arrays.asList(this.config.getString("locale"), "en")).build(); + } else { + mmreader = new DatabaseReader.Builder(this.databaseFile).build(); + } + } catch (IOException ex) { + this.getLogger().warning("Failed to read GeoIP database", ex); + this.setEnabled(false); + return; + } + this.getServer().getPluginManager().registerEvents(new GeoIPListener(this, mmreader), this); + this.getServer().getCommandMap().register("geoip", new GeoIPCommand(this)); + } + + private void downloadDatabase() { + try { + String url; + if (this.config.getBoolean("database.show-cities", false)) { + url = this.config.getString("database.download-url-city"); + } else { + url = this.config.getString("database.download-url"); + } + if (url == null || url.isEmpty()) { + this.getLogger().warning("GeoIP download url is empty."); + return; + } + this.getLogger().info("Downloading GeoIP database... this might take a while."); + URL downloadUrl = new URL(url); + URLConnection conn = downloadUrl.openConnection(); + conn.setConnectTimeout(10000); + conn.connect(); + InputStream input = conn.getInputStream(); + OutputStream output = new FileOutputStream(this.databaseFile); + byte[] buffer = new byte[2048]; + if (url.endsWith(".gz")) { + input = new GZIPInputStream(input); + if (url.endsWith(".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; + 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 (MalformedURLException ex) { + this.getLogger().warning("GeoIP download url is invalid", ex); + } catch (IOException ex) { + this.getLogger().warning("Failed to open connection", ex); + } + } +} diff --git a/src/main/java/cn/wode490390/nukkit/geoip/GeoIPCommand.java b/src/main/java/cn/wode490390/nukkit/geoip/GeoIPCommand.java new file mode 100644 index 0000000..e00faef --- /dev/null +++ b/src/main/java/cn/wode490390/nukkit/geoip/GeoIPCommand.java @@ -0,0 +1,60 @@ +package cn.wode490390.nukkit.geoip; + +import cn.nukkit.Player; +import cn.nukkit.Server; +import cn.nukkit.command.Command; +import cn.nukkit.command.CommandSender; +import cn.nukkit.command.PluginIdentifiableCommand; +import cn.nukkit.command.data.CommandParamType; +import cn.nukkit.command.data.CommandParameter; +import cn.nukkit.lang.TranslationContainer; +import cn.nukkit.plugin.Plugin; +import cn.nukkit.utils.TextFormat; + +public class GeoIPCommand extends Command implements PluginIdentifiableCommand { + + private final Plugin plugin; + + public GeoIPCommand(Plugin plugin) { + super("geoip", "Querys the GeoIP location of a player", "/geoip "); + this.setPermission("geoip.show"); + this.getCommandParameters().clear(); + this.addCommandParameters("default", new CommandParameter[]{ + new CommandParameter("player", CommandParamType.TARGET, false) + }); + this.plugin = plugin; + } + + @Override + public boolean execute(CommandSender sender, String label, String[] args) { + if (!this.plugin.isEnabled() || !this.testPermission(sender)) { + return false; + } + if (args.length > 0) { + Player player = Server.getInstance().getPlayer(args[0]); + if (player != null) { + String geoLocation = GeoIP.query(player); + if (geoLocation != null) { + String[] ip = player.getAddress().split("\\."); + try { + sender.sendMessage(TextFormat.colorize("&6Player &c" + player.getDisplayName() + " &6comes from &c" + geoLocation + "&6. (IP:&c" + (sender.hasPermission("geoip.show.fullip") ? player.getAddress() : ip[0] + ".*.*." + ip[3]) + "&6)")); + return true; + } catch (Exception ex) { + + } + } + sender.sendMessage(TextFormat.colorize("&6Player &c" + player.getDisplayName() + " &6comes from &aan unknown country&6.")); + } else { + sender.sendMessage(new TranslationContainer("commands.generic.player.notFound")); + } + } else { + sender.sendMessage(new TranslationContainer("commands.generic.usage", this.getUsage())); + } + return true; + } + + @Override + public Plugin getPlugin() { + return this.plugin; + } +} diff --git a/src/main/java/cn/wode490390/nukkit/geoip/GeoIPListener.java b/src/main/java/cn/wode490390/nukkit/geoip/GeoIPListener.java new file mode 100644 index 0000000..f67cd37 --- /dev/null +++ b/src/main/java/cn/wode490390/nukkit/geoip/GeoIPListener.java @@ -0,0 +1,116 @@ +package cn.wode490390.nukkit.geoip; + +import cn.nukkit.Player; +import cn.nukkit.event.EventHandler; +import cn.nukkit.event.EventPriority; +import cn.nukkit.event.Listener; +import cn.nukkit.event.player.PlayerJoinEvent; +import cn.nukkit.scheduler.NukkitRunnable; +import cn.nukkit.utils.TextFormat; +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 java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; + +public class GeoIPListener implements Listener { + + private final GeoIP plugin; + private final DatabaseReader mmreader; + + GeoIPListener(GeoIP plugin, DatabaseReader mmreader) { + this.plugin = plugin; + this.mmreader = mmreader; + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerJoin(PlayerJoinEvent event) { + new NukkitRunnable() { + @Override + public void run() { + try { + delayedJoin(event.getPlayer()); + } catch (UnknownHostException ex) { + plugin.getLogger().error("Invalid ip address", ex); + } + } + }.runTaskAsynchronously(this.plugin); + } + + private void delayedJoin(Player player) throws UnknownHostException { + if (player.hasPermission("geoip.hide")) { + return; + } + InetAddress address = InetAddress.getByName(player.getAddress()); + StringBuilder sb = new StringBuilder(); + try { + if (this.plugin.config.getBoolean("database.show-cities", false)) { + CityResponse response = this.mmreader.city(address); + if (response == null) { + return; + } + String city; + String region; + 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 { + CountryResponse response = this.mmreader.country(address); + sb.append(response.getCountry().getName()); + } + } catch (AddressNotFoundException ex) { + String msg = TextFormat.colorize("&6Player &c" + player.getDisplayName() + " &6comes from &aan unknown country&6."); + if (checkIfLocal(address)) { + this.plugin.getServer().getOnlinePlayers().values().stream() + .filter(p -> p.hasPermission("geoip.show")) + .forEach(p -> p.sendMessage(msg)); + return; + } + this.plugin.getLogger().info("Unknown ip: " + player.getAddress() + " (" + player.getName() + ")"); + } catch (IOException | GeoIp2Exception ex) { + // GeoIP2 API forced this when address not found in their DB. jar will not complied without this. + this.plugin.getLogger().warning("Failed to read GeoIP database: " + ex.getLocalizedMessage()); + } + GeoIP.setGeoLocation(player, sb.toString()); + if (this.plugin.config.getBoolean("show-on-login", true)) { + String template = TextFormat.colorize("&6Player &c" + player.getDisplayName() + " &6comes from &c" + sb.toString() + "&6. (IP:&c%ip%&6)"); + String[] ip = player.getAddress().split("\\."); + String anonym; + try { + anonym = ip[0] + ".*.*." + ip[3]; + } catch (Exception ex) { + throw new UnknownHostException(ex.getLocalizedMessage()); + } + String msg = template.replace("%ip%", player.getAddress()); + String anonymousMsg = template.replace("%ip%", anonym); + player.getServer().getOnlinePlayers().values().stream() + .filter(p -> p.hasPermission("geoip.show")) + .forEach(p -> p.sendMessage(p.hasPermission("geoip.show.fullip") ? msg : anonymousMsg)); + } + } + + private boolean checkIfLocal(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 (SocketException e) { + return false; + } + } +} diff --git a/src/main/java/cn/wode490390/nukkit/geoip/MetricsLite.java b/src/main/java/cn/wode490390/nukkit/geoip/MetricsLite.java new file mode 100644 index 0000000..56eba67 --- /dev/null +++ b/src/main/java/cn/wode490390/nukkit/geoip/MetricsLite.java @@ -0,0 +1,366 @@ +package cn.wode490390.nukkit.geoip; + +import cn.nukkit.Server; +import cn.nukkit.plugin.Plugin; +import cn.nukkit.plugin.service.NKServiceManager; +import cn.nukkit.plugin.service.RegisteredServiceProvider; +import cn.nukkit.plugin.service.ServicePriority; +import cn.nukkit.utils.Config; +import com.google.common.base.Preconditions; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import javax.net.ssl.HttpsURLConnection; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.UUID; +import java.util.zip.GZIPOutputStream; + +/** + * bStats collects some data for plugin authors. + *

+ * Check out https://bStats.org/ to learn more about bStats! + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public class MetricsLite { + + static { + // You can use the property to disable the check in your test environment + if (System.getProperty("bstats.relocatecheck") == null || !System.getProperty("bstats.relocatecheck").equals("false")) { + // Maven's Relocate is clever and changes strings, too. So we have to use this little "trick" ... :D + final String defaultPackage = new String(new byte[]{'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's', '.', 'n', 'u', 'k', 'k', 'i', 't'}); + final String examplePackage = new String(new byte[]{'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); + // We want to make sure nobody just copy & pastes the example and use the wrong package names + if (MetricsLite.class.getPackage().getName().equals(defaultPackage) || MetricsLite.class.getPackage().getName().equals(examplePackage)) { + throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); + } + } + } + + // The version of this bStats class + public static final int B_STATS_VERSION = 1; + + // The url to which the data is sent + private static final String URL = "https://bStats.org/submitData/bukkit"; + + // Is bStats enabled on this server? + private boolean enabled; + + // Should failed requests be logged? + private static boolean logFailedRequests; + + // Should the sent data be logged? + private static boolean logSentData; + + // Should the response text be logged? + private static boolean logResponseStatusText; + + // The uuid of the server + private static String serverUUID; + + // The plugin + private final Plugin plugin; + + /** + * Class constructor. + * + * @param plugin The plugin which stats should be submitted. + */ + public MetricsLite(Plugin plugin) { + Preconditions.checkNotNull(plugin); + this.plugin = plugin; + + // Get the config file + File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); + File configFile = new File(bStatsFolder, "config.yml"); + Config config = new Config(configFile); + + // Check the config + LinkedHashMap map = (LinkedHashMap) config.getAll(); + // Every server gets it's unique random id. + if (!config.isString("serverUuid")) { + map.put("serverUuid", UUID.randomUUID().toString()); + } else { + try { + // Check the UUID + UUID.fromString(config.getString("serverUuid")); + } catch (Exception ignored){ + map.put("serverUuid", UUID.randomUUID().toString()); + } + } + // Add default values + if (!config.isBoolean("enabled")) { + map.put("enabled", true); + } + // Should failed request be logged? + if (!config.isBoolean("logFailedRequests")) { + map.put("logFailedRequests", false); + } + // Should the sent data be logged? + if (!config.isBoolean("logSentData")) { + map.put("logSentData", false); + } + // Should the response text be logged? + if (!config.isBoolean("logResponseStatusText")) { + map.put("logResponseStatusText", false); + } + config.setAll(map); + config.save(); + + // Load the data + enabled = config.getBoolean("enabled", true); + serverUUID = config.getString("serverUuid"); + logFailedRequests = config.getBoolean("logFailedRequests", false); + logSentData = config.getBoolean("logSentData", false); + logResponseStatusText = config.getBoolean("logResponseStatusText", false); + + if (enabled) { + boolean found = false; + // Search for all other bStats Metrics classes to see if we are the first one + for (Class service : Server.getInstance().getServiceManager().getKnownService()) { + try { + service.getField("B_STATS_VERSION"); // Our identifier :) + found = true; // We aren't the first + break; + } catch (NoSuchFieldException ignored) { } + } + // Register our service + Server.getInstance().getServiceManager().register(MetricsLite.class, this, plugin, ServicePriority.NORMAL); + if (!found) { + // We are the first! + startSubmitting(); + } + } + } + + /** + * Checks if bStats is enabled. + * + * @return Whether bStats is enabled or not. + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Starts the Scheduler which submits our data every 30 minutes. + */ + private void startSubmitting() { + final Timer timer = new Timer(true); // We use a timer cause want to be independent from the server tps + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + if (!plugin.isEnabled()) { // Plugin was disabled + timer.cancel(); + return; + } + // Nevertheless we want our code to run in the Nukkit main thread, so we have to use the Nukkit scheduler + // Don't be afraid! The connection to the bStats server is still async, only the stats collection is sync ;) + Server.getInstance().getScheduler().scheduleTask(plugin, () -> submitData()); + } + }, 1000 * 60 * 5, 1000 * 60 * 30); + // Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough time to start + // WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted! + // WARNING: Just don't do it! + } + + /** + * Gets the plugin specific data. + * This method is called using Reflection. + * + * @return The plugin specific data. + */ + public JsonObject getPluginData() { + JsonObject data = new JsonObject(); + + String pluginName = plugin.getName(); + String pluginVersion = plugin.getDescription().getVersion(); + + data.addProperty("pluginName", pluginName); // Append the name of the plugin + data.addProperty("pluginVersion", pluginVersion); // Append the version of the plugin + + JsonArray customCharts = new JsonArray(); + data.add("customCharts", customCharts); + + return data; + } + + /** + * Gets the server specific data. + * + * @return The server specific data. + */ + private JsonObject getServerData() { + // Minecraft specific data + int playerAmount = Server.getInstance().getOnlinePlayers().size(); + int onlineMode = Server.getInstance().getPropertyBoolean("xbox-auth", false) ? 1 : 0; + String minecraftVersion = Server.getInstance().getVersion(); + String softwareName = Server.getInstance().getName(); + + // OS/Java specific data + String javaVersion = System.getProperty("java.version"); + String osName = System.getProperty("os.name"); + String osArch = System.getProperty("os.arch"); + String osVersion = System.getProperty("os.version"); + int coreCount = Runtime.getRuntime().availableProcessors(); + + JsonObject data = new JsonObject(); + + data.addProperty("serverUUID", serverUUID); + + data.addProperty("playerAmount", playerAmount); + data.addProperty("onlineMode", onlineMode); + data.addProperty("bukkitVersion", minecraftVersion); + data.addProperty("bukkitName", softwareName); + + data.addProperty("javaVersion", javaVersion); + data.addProperty("osName", osName); + data.addProperty("osArch", osArch); + data.addProperty("osVersion", osVersion); + data.addProperty("coreCount", coreCount); + + return data; + } + + /** + * Collects the data and sends it afterwards. + */ + @SuppressWarnings("unchecked") + private void submitData() { + final JsonObject data = getServerData(); + + JsonArray pluginData = new JsonArray(); + // Search for all other bStats Metrics classes to get their plugin data + Server.getInstance().getServiceManager().getKnownService().forEach((service) -> { + try { + service.getField("B_STATS_VERSION"); // Our identifier :) + + List> providers = null; + try { + Field field = Field.class.getDeclaredField("modifiers"); + field.setAccessible(true); + Field handle = NKServiceManager.class.getDeclaredField("handle"); + field.setInt(handle, handle.getModifiers() & ~Modifier.FINAL); + handle.setAccessible(true); + providers = ((Map, List>>) handle.get((NKServiceManager) (Server.getInstance().getServiceManager()))).get(service); + } catch(IllegalAccessException | IllegalArgumentException | SecurityException e) { + // Something went wrong! :( + if (logFailedRequests) { + plugin.getLogger().warning("Failed to link to metrics class " + service.getName(), e); + } + } + + if (providers != null) { + for (RegisteredServiceProvider provider : providers) { + try { + Object plugin = provider.getService().getMethod("getPluginData").invoke(provider.getProvider()); + if (plugin instanceof JsonObject) { + pluginData.add((JsonElement) plugin); + } + } catch (SecurityException | NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ignored) { } + } + } + } catch (NoSuchFieldException ignored) { } + }); + + data.add("plugins", pluginData); + + // Create a new thread for the connection to the bStats server + new Thread(() -> { + try { + // Send the data + sendData(plugin, data); + } catch (Exception e) { + // Something went wrong! :( + if (logFailedRequests) { + plugin.getLogger().warning("Could not submit plugin stats of " + plugin.getName(), e); + } + } + }).start(); + } + + /** + * Sends the data to the bStats server. + * + * @param plugin Any plugin. It's just used to get a logger instance. + * @param data The data to send. + * @throws Exception If the request failed. + */ + private static void sendData(Plugin plugin, JsonObject data) throws Exception { + Preconditions.checkNotNull(data); + if (Server.getInstance().isPrimaryThread()) { + throw new IllegalAccessException("This method must not be called from the main thread!"); + } + if (logSentData) { + plugin.getLogger().info("Sending data to bStats: " + data.toString()); + } + HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection(); + + // Compress the data to save bandwidth + byte[] compressedData = compress(data.toString()); + + // Add headers + connection.setRequestMethod("POST"); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request + connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); + connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format + connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION); + + // Send data + connection.setDoOutput(true); + try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { + outputStream.write(compressedData); + outputStream.flush(); + } + + InputStream inputStream = connection.getInputStream(); + StringBuilder builder; + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { + builder = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + builder.append(line); + } + } + if (logResponseStatusText) { + plugin.getLogger().info("Sent data to bStats and received response: " + builder.toString()); + } + } + + /** + * Gzips the given String. + * + * @param str The string to gzip. + * @return The gzipped String. + * @throws IOException If the compression failed. + */ + private static byte[] compress(final String str) throws IOException { + if (str == null) { + return null; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { + gzip.write(str.getBytes(StandardCharsets.UTF_8)); + } + return outputStream.toByteArray(); + } + +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..e08551f --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,15 @@ +database: + show-cities: false + download-if-missing: true + # Url for country + download-url: "https://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz" + # Url for cities + download-url-city: "https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz" + update: + enable: true + by-every-x-days: 30 +show-on-login: true +# "enable-locale" enables locale on geolocation display. +enable-locale: true +# Not all languages are supported. See https://dev.maxmind.com/geoip/geoip2/web-services/#Languages +locale: en diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..eb94f84 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,18 @@ +main: cn.wode490390.nukkit.geoip.GeoIP +name: "GeoIP" +description: "GeoIP provides an approximate lookup of where your players come from, based on their public IP and public geographical databases." +author: "wode490390" +website: "http://wode490390.cn/" +version: "${pom.version}" +api: ["1.0.0"] +load: POSTWORLD +permissions: + geoip.hide: + description: "Allows player to hide player's country and city from people who have permission geoip.show" + default: false + geoip.show: + description: "Shows the GeoIP location of a player." + default: op + geoip.show.fullip: + description: "Shows the full ip address of a player." + default: false